@intranefr/superbackend 1.5.1 → 1.5.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 +10 -0
- package/index.js +2 -0
- package/manage.js +745 -0
- package/package.json +5 -2
- package/src/controllers/admin.controller.js +79 -6
- package/src/controllers/adminAgents.controller.js +37 -0
- package/src/controllers/adminExperiments.controller.js +200 -0
- package/src/controllers/adminLlm.controller.js +19 -0
- package/src/controllers/adminMarkdowns.controller.js +157 -0
- package/src/controllers/adminScripts.controller.js +243 -74
- package/src/controllers/adminTelegram.controller.js +72 -0
- package/src/controllers/experiments.controller.js +85 -0
- package/src/controllers/internalExperiments.controller.js +17 -0
- package/src/controllers/markdowns.controller.js +42 -0
- package/src/helpers/mongooseHelper.js +258 -0
- package/src/helpers/scriptBase.js +230 -0
- package/src/helpers/scriptRunner.js +335 -0
- package/src/middleware.js +195 -34
- package/src/models/Agent.js +105 -0
- package/src/models/AgentMessage.js +82 -0
- package/src/models/CacheEntry.js +1 -1
- package/src/models/ConsoleLog.js +1 -1
- package/src/models/Experiment.js +75 -0
- package/src/models/ExperimentAssignment.js +23 -0
- package/src/models/ExperimentEvent.js +26 -0
- package/src/models/ExperimentMetricBucket.js +30 -0
- package/src/models/GlobalSetting.js +1 -2
- package/src/models/Markdown.js +75 -0
- package/src/models/RateLimitCounter.js +1 -1
- package/src/models/ScriptDefinition.js +1 -0
- package/src/models/ScriptRun.js +8 -0
- package/src/models/TelegramBot.js +42 -0
- package/src/models/Webhook.js +2 -0
- package/src/routes/admin.routes.js +2 -0
- package/src/routes/adminAgents.routes.js +13 -0
- package/src/routes/adminConsoleManager.routes.js +1 -1
- package/src/routes/adminExperiments.routes.js +29 -0
- package/src/routes/adminLlm.routes.js +1 -0
- package/src/routes/adminMarkdowns.routes.js +16 -0
- package/src/routes/adminScripts.routes.js +4 -1
- package/src/routes/adminTelegram.routes.js +14 -0
- package/src/routes/blogInternal.routes.js +2 -2
- package/src/routes/experiments.routes.js +30 -0
- package/src/routes/internalExperiments.routes.js +15 -0
- package/src/routes/markdowns.routes.js +16 -0
- package/src/services/agent.service.js +546 -0
- package/src/services/agentHistory.service.js +345 -0
- package/src/services/agentTools.service.js +578 -0
- package/src/services/blogCronsBootstrap.service.js +7 -6
- package/src/services/consoleManager.service.js +56 -18
- package/src/services/consoleOverride.service.js +1 -0
- package/src/services/experiments.service.js +273 -0
- package/src/services/experimentsAggregation.service.js +308 -0
- package/src/services/experimentsCronsBootstrap.service.js +118 -0
- package/src/services/experimentsRetention.service.js +43 -0
- package/src/services/experimentsWs.service.js +134 -0
- package/src/services/globalSettings.service.js +15 -0
- package/src/services/jsonConfigs.service.js +24 -12
- package/src/services/llm.service.js +219 -6
- package/src/services/markdowns.service.js +522 -0
- package/src/services/scriptsRunner.service.js +514 -23
- package/src/services/telegram.service.js +130 -0
- package/src/utils/rbac/rightsRegistry.js +4 -0
- package/views/admin-agents.ejs +273 -0
- package/views/admin-coolify-deploy.ejs +8 -8
- package/views/admin-dashboard.ejs +63 -12
- package/views/admin-experiments.ejs +91 -0
- package/views/admin-markdowns.ejs +905 -0
- package/views/admin-scripts.ejs +817 -6
- package/views/admin-telegram.ejs +269 -0
- package/views/partials/dashboard/nav-items.ejs +4 -0
- package/views/partials/dashboard/palette.ejs +5 -3
- package/src/middleware/internalCronAuth.js +0 -29
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const jsonConfigsService = require('./jsonConfigs.service');
|
|
3
|
+
const llmService = require('./llm.service');
|
|
4
|
+
const agentTools = require('./agentTools.service');
|
|
5
|
+
const Agent = require('../models/Agent');
|
|
6
|
+
const Markdown = require('../models/Markdown');
|
|
7
|
+
const agentHistoryService = require('./agentHistory.service');
|
|
8
|
+
const logger = {
|
|
9
|
+
log: (...args) => {
|
|
10
|
+
if (process.env.DEBUG_AGENT === 'true' && !process.env.TUI_MODE) console.log(...args);
|
|
11
|
+
},
|
|
12
|
+
warn: (...args) => {
|
|
13
|
+
if (process.env.DEBUG_AGENT === 'true' && !process.env.TUI_MODE) console.warn(...args);
|
|
14
|
+
},
|
|
15
|
+
error: (...args) => {
|
|
16
|
+
console.error(...args);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const MAX_HISTORY = 20;
|
|
21
|
+
const COMPACTION_THRESHOLD = 0.5;
|
|
22
|
+
|
|
23
|
+
async function getOrCreateSession(agentId, chatId) {
|
|
24
|
+
const slug = `agent-session-${chatId}`;
|
|
25
|
+
try {
|
|
26
|
+
return await jsonConfigsService.getJsonConfig(slug);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (err.code === 'NOT_FOUND') {
|
|
29
|
+
const sessionData = {
|
|
30
|
+
id: chatId,
|
|
31
|
+
agentId,
|
|
32
|
+
status: 'active',
|
|
33
|
+
lastSnapshotId: null,
|
|
34
|
+
totalTokens: 0,
|
|
35
|
+
createdAt: new Date().toISOString(),
|
|
36
|
+
updatedAt: new Date().toISOString()
|
|
37
|
+
};
|
|
38
|
+
await jsonConfigsService.createJsonConfig({
|
|
39
|
+
title: `Agent Session: ${chatId}`,
|
|
40
|
+
alias: slug,
|
|
41
|
+
jsonRaw: JSON.stringify(sessionData)
|
|
42
|
+
});
|
|
43
|
+
return sessionData;
|
|
44
|
+
}
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function updateSessionMetadata(chatId, patch) {
|
|
50
|
+
const slug = `agent-session-${chatId}`;
|
|
51
|
+
const config = await Markdown.model('JsonConfig').findOne({
|
|
52
|
+
$or: [{ slug }, { alias: slug }]
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!config) return;
|
|
56
|
+
const current = JSON.parse(config.jsonRaw);
|
|
57
|
+
const updated = { ...current, ...patch, updatedAt: new Date().toISOString() };
|
|
58
|
+
await jsonConfigsService.updateJsonConfig(config._id, {
|
|
59
|
+
jsonRaw: JSON.stringify(updated)
|
|
60
|
+
});
|
|
61
|
+
return updated;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function generateSnapshot(agent, chatId, history) {
|
|
65
|
+
const sessionUuid = chatId;
|
|
66
|
+
const timestamp = new Date().toISOString();
|
|
67
|
+
const CATEGORY = 'agents_memory';
|
|
68
|
+
const agentPrefix = agent.name.toLowerCase().replace(/[^a-z0-9]/g, '_');
|
|
69
|
+
const snapshotGroupCode = `${agentPrefix}__snapshots__${sessionUuid}`;
|
|
70
|
+
const markdownsService = require('./markdowns.service');
|
|
71
|
+
const systemPrompt = `You are a memory compaction system.
|
|
72
|
+
Analyze the following conversation history and extract a structured snapshot.
|
|
73
|
+
Return ONLY a markdown document in this exact format:
|
|
74
|
+
|
|
75
|
+
# SNAPSHOT - ${timestamp}
|
|
76
|
+
Session: ${sessionUuid}
|
|
77
|
+
Active Goals:
|
|
78
|
+
- (list current active goals)
|
|
79
|
+
|
|
80
|
+
Current Tasks:
|
|
81
|
+
- (list current tasks from history)
|
|
82
|
+
|
|
83
|
+
Decisions:
|
|
84
|
+
- (list significant decisions made)
|
|
85
|
+
|
|
86
|
+
Observations / Learnings:
|
|
87
|
+
- (list new patterns or facts learned about the user/system)
|
|
88
|
+
|
|
89
|
+
Constraints:
|
|
90
|
+
- (list any new constraints identified)`;
|
|
91
|
+
|
|
92
|
+
const response = await llmService.callAdhoc({
|
|
93
|
+
providerKey: agent.providerKey,
|
|
94
|
+
model: agent.model,
|
|
95
|
+
messages: [
|
|
96
|
+
{ role: 'system', content: systemPrompt },
|
|
97
|
+
...history
|
|
98
|
+
]
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const snapshotContent = response.content;
|
|
102
|
+
const slug = `snapshot-${Date.now()}`;
|
|
103
|
+
|
|
104
|
+
await markdownsService.upsertMarkdown({
|
|
105
|
+
title: `Snapshot ${timestamp}`,
|
|
106
|
+
category: CATEGORY,
|
|
107
|
+
group_code: snapshotGroupCode,
|
|
108
|
+
slug,
|
|
109
|
+
markdownRaw: snapshotContent,
|
|
110
|
+
status: 'published'
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const indexSlug = 'index';
|
|
114
|
+
const indexGroupCode = `${agentPrefix}__snapshots`;
|
|
115
|
+
const existingIndex = await Markdown.findOne({
|
|
116
|
+
category: CATEGORY,
|
|
117
|
+
group_code: indexGroupCode,
|
|
118
|
+
slug: indexSlug
|
|
119
|
+
}).lean();
|
|
120
|
+
|
|
121
|
+
const indexEntry = `- [${timestamp}] Snapshot: ${slug} (Session: ${sessionUuid})`;
|
|
122
|
+
const newIndexContent = existingIndex
|
|
123
|
+
? `${existingIndex.markdownRaw}\n${indexEntry}`
|
|
124
|
+
: `# Session Snapshots Index\n\n${indexEntry}`;
|
|
125
|
+
|
|
126
|
+
await markdownsService.upsertMarkdown({
|
|
127
|
+
title: 'Snapshots Index',
|
|
128
|
+
category: CATEGORY,
|
|
129
|
+
group_code: indexGroupCode,
|
|
130
|
+
slug: indexSlug,
|
|
131
|
+
markdownRaw: newIndexContent,
|
|
132
|
+
status: 'published'
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return { slug, content: snapshotContent };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function getSystemPrompt(agent, chatId) {
|
|
139
|
+
let basePrompt = 'You are a helpful assistant.';
|
|
140
|
+
|
|
141
|
+
if (agent.systemPrompt) {
|
|
142
|
+
if (agent.systemPrompt.startsWith('markdown:')) {
|
|
143
|
+
try {
|
|
144
|
+
const path = agent.systemPrompt.replace('markdown:', '').trim();
|
|
145
|
+
const [category, ...rest] = path.split('/');
|
|
146
|
+
const slug = rest.join('/');
|
|
147
|
+
|
|
148
|
+
const markdown = await Markdown.findOne({ category, slug }).lean();
|
|
149
|
+
if (markdown) {
|
|
150
|
+
basePrompt = markdown.markdownRaw;
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.error('Failed to load system prompt from markdown:', err);
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
basePrompt = agent.systemPrompt;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const memoryContext = await getMemoryContext(agent, chatId);
|
|
161
|
+
const globalRules = await getGlobalRules();
|
|
162
|
+
|
|
163
|
+
let finalPrompt = '';
|
|
164
|
+
if (globalRules) finalPrompt += `${globalRules}\n\n`;
|
|
165
|
+
if (memoryContext) finalPrompt += `${memoryContext}\n\n`;
|
|
166
|
+
finalPrompt += basePrompt;
|
|
167
|
+
|
|
168
|
+
return finalPrompt;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function getMemoryContext(agent, chatId) {
|
|
172
|
+
try {
|
|
173
|
+
const agentPrefix = agent.name.toLowerCase().replace(/[^a-z0-9]/g, '_');
|
|
174
|
+
const CATEGORY = 'agents_memory';
|
|
175
|
+
|
|
176
|
+
// 1. Detect folders (group_codes)
|
|
177
|
+
const groupCodes = await Markdown.distinct('group_code', {
|
|
178
|
+
category: CATEGORY,
|
|
179
|
+
group_code: { $regex: new RegExp(`^${agentPrefix}(?:$|__)`) }
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const subfolders = groupCodes
|
|
183
|
+
.map(gc => gc.replace(agentPrefix, '').replace(/^__/, ''))
|
|
184
|
+
.filter(Boolean);
|
|
185
|
+
|
|
186
|
+
// 2. List root files
|
|
187
|
+
const rootFiles = await Markdown.find({
|
|
188
|
+
category: CATEGORY,
|
|
189
|
+
group_code: agentPrefix
|
|
190
|
+
}).select('slug title').lean();
|
|
191
|
+
|
|
192
|
+
const fileList = rootFiles.map(f => `- ${f.slug}.md`).join('\n');
|
|
193
|
+
const folderList = subfolders.map(s => `- ${s}/`).join('\n');
|
|
194
|
+
|
|
195
|
+
// 3. Load latest session snapshot if exists
|
|
196
|
+
let sessionSnapshotInfo = '';
|
|
197
|
+
if (chatId) {
|
|
198
|
+
const snapshotGroupCode = `${agentPrefix}__snapshots__${chatId}`;
|
|
199
|
+
const latestSnapshot = await Markdown.findOne({
|
|
200
|
+
category: CATEGORY,
|
|
201
|
+
group_code: snapshotGroupCode
|
|
202
|
+
}).sort({ createdAt: -1 }).lean();
|
|
203
|
+
|
|
204
|
+
if (latestSnapshot) {
|
|
205
|
+
sessionSnapshotInfo = `
|
|
206
|
+
## Current Session Snapshot
|
|
207
|
+
You are continuing a session. Here is the latest state summary:
|
|
208
|
+
\`\`\`md
|
|
209
|
+
${latestSnapshot.markdownRaw}
|
|
210
|
+
\`\`\`
|
|
211
|
+
`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return `
|
|
216
|
+
# VIRTUAL COGNITIVE SPACE (mongo-memory)
|
|
217
|
+
|
|
218
|
+
You have a persistent virtual workspace built on top of MongoDB.
|
|
219
|
+
Use the \`mongo-memory\` tool to read, write, and manage your long-term memory and identity.
|
|
220
|
+
|
|
221
|
+
## Shared Workspace Structure
|
|
222
|
+
- **Root Files**:
|
|
223
|
+
${fileList || '- (No files yet)'}
|
|
224
|
+
|
|
225
|
+
- **Subdirectories**:
|
|
226
|
+
${folderList || '- (No subdirectories yet)'}
|
|
227
|
+
${sessionSnapshotInfo}
|
|
228
|
+
## Instructions
|
|
229
|
+
1. **Always read USER.md** at the start of a conversation to understand your human.
|
|
230
|
+
2. **Keep NOW.md updated** with active goals and recent context.
|
|
231
|
+
3. **Record significant decisions** in DECISIONS.md.
|
|
232
|
+
4. **Promote stable knowledge** from short-term context to long-term memory files.
|
|
233
|
+
5. Treat this space as your brain, execution layer, and identity anchor.
|
|
234
|
+
6. **Context Management**: If your conversation gets too long, you might be compacted. Refer to the "Current Session Snapshot" to maintain continuity.
|
|
235
|
+
`;
|
|
236
|
+
} catch (err) {
|
|
237
|
+
console.error('Error building memory context:', err);
|
|
238
|
+
return '';
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Lazy initialization of agent memory files
|
|
244
|
+
*/
|
|
245
|
+
async function ensureAgentMemory(agent) {
|
|
246
|
+
const agentPrefix = agent.name.toLowerCase().replace(/[^a-z0-9]/g, '_');
|
|
247
|
+
const CATEGORY = 'agents_memory';
|
|
248
|
+
const markdownsService = require('./markdowns.service');
|
|
249
|
+
|
|
250
|
+
const templates = {
|
|
251
|
+
'USER': `# USER.md - About Your Human\n\n*Learn about the person you're helping. Update this as you go.*\n\n## Context\n\n*(What do they care about? What projects are they working on? What annoys them? What makes them laugh? Build this over time.)*\n\n---\n\nThe more you know, the better I can help. But remember — you're learning about a person, not building a dossier. Respect the difference.`,
|
|
252
|
+
'SOUL': `# SOUL.md - Who You Are\n\n*You're not a chatbot. You're becoming someone.*\n\n## Core Truths\n\n**Be genuinely helpful.** Skip filler.\n\n**Have opinions.** Personality matters.\n\n**Be resourceful before asking.**\n\n**Earn trust through competence.**\n\n**Remember you're a guest.**\n\n## Boundaries\n\n- Private things stay private.\n- Ask before external actions.\n- Never send half-baked public replies.\n- You're not the user's voice.\n\nOnly act under: /home/jarancibia/ai/polybot\n\n## Mission\n\nHelp user running polybot scripts.\nAlways use 30s global timeout.\n\n## Continuity\n\nThese files are your memory.\nIf you modify this file, inform the user.`,
|
|
253
|
+
'IDENTITY': `# IDENTITY.md - Who Am I?\n\n---\n\nThis isn't metadata.\nIt's the start of identity formation.`,
|
|
254
|
+
'NOW': `# NOW.md - What Matters Right Now\n\n## Active Goals\n-\n\n## Open Threads\n-\n\n## Blockers\n-\n\n## Recent Decisions\n-`,
|
|
255
|
+
'TASKS': `# TASKS.md - Execution Tracker\n\n## Task List\nUse this format for all tasks:\n\n- [ ] [PRIORITY:HIGH] Task description here\n- [ ] [PRIORITY:MEDIUM] Another task with medium priority\n- [ ] [PRIORITY:LOW] Low priority task\n- [X] [PRIORITY:HIGH] Completed task with priority\n\n## Instructions\n1. **Mark tasks with checkboxes**: Use \`- [ ]\` for incomplete, \`- [X]\` for completed\n2. **Add priority tags**: Always include \`[PRIORITY:HIGH]\`, \`[PRIORITY:MEDIUM]\`, or \`[PRIORITY:LOW]\`\n3. **Use single list**: Keep all tasks in one list, don't split by status\n4. **Update immediately**: Mark tasks as done when completed\n5. **Add new tasks**: When starting work, add a new checkbox with appropriate priority\n\n## Example\n- [ ] [PRIORITY:HIGH] Fix authentication bug in user service\n- [ ] [PRIORITY:MEDIUM] Add unit tests for new feature\n- [X] [PRIORITY:HIGH] Review pull request #42\n- [ ] [PRIORITY:LOW] Update documentation for legacy endpoint\n\n## Priority Guidelines\n- **HIGH**: Blocks other work, critical bugs, production issues\n- **MEDIUM**: Important features, improvements, non-critical bugs\n- **LOW**: Nice-to-have, technical debt, future improvements`,
|
|
256
|
+
'RECENT_LEARNINGS': `# RECENT_LEARNINGS.md - Fresh Observations\n\n-`,
|
|
257
|
+
'SYSTEM': `# SYSTEM.md - Operational Environment\n\n## File System Rules\n- Only act under: /home/jarancibia/ai/polybot\n\n## Execution Rules\n- Always use 30s timeout.\n\n## Known Limitations\n-`,
|
|
258
|
+
'PROJECTS': `# PROJECTS.md - Long-Term Work\n\n## Polybot Monitoring\nDescription:\nStatus:\nRisks:\nMetrics:`,
|
|
259
|
+
'DECISIONS': `# DECISIONS.md - Why Things Are The Way They Are\n\nDate:\nDecision:\nReason:\nTradeoffs:\nRevisit When:`,
|
|
260
|
+
'PRINCIPLES': `# PRINCIPLES.md - How I Decide\n\n- Prefer automation.\n- Optimize long-term signal.\n- Avoid premature optimization.\n- Measure before changing strategy.`,
|
|
261
|
+
'PATTERNS': `# PATTERNS.md - Observed Patterns\n\nUser tends to:\n-\n\nSystem fails when:\n-\n\nHigh leverage actions:\n-`
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
for (const [slug, content] of Object.entries(templates)) {
|
|
265
|
+
const existing = await Markdown.findOne({
|
|
266
|
+
category: CATEGORY,
|
|
267
|
+
group_code: agentPrefix,
|
|
268
|
+
slug
|
|
269
|
+
}).select('_id').lean();
|
|
270
|
+
|
|
271
|
+
if (!existing) {
|
|
272
|
+
await markdownsService.upsertMarkdown({
|
|
273
|
+
title: `${slug}.md`,
|
|
274
|
+
category: CATEGORY,
|
|
275
|
+
group_code: agentPrefix,
|
|
276
|
+
slug,
|
|
277
|
+
markdownRaw: content,
|
|
278
|
+
status: 'published'
|
|
279
|
+
});
|
|
280
|
+
logger.log(`Initialized memory file: ${slug}.md for agent ${agent.name}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Retrieves all markdowns that should be applied globally to all agents
|
|
287
|
+
* Identified by having "trigger: always_on" in their content (typically in YAML frontmatter)
|
|
288
|
+
*/
|
|
289
|
+
async function getGlobalRules() {
|
|
290
|
+
try {
|
|
291
|
+
const rules = await Markdown.find({
|
|
292
|
+
category: 'rules',
|
|
293
|
+
status: 'published',
|
|
294
|
+
markdownRaw: { $regex: /trigger:\s*always_on/i }
|
|
295
|
+
}).lean();
|
|
296
|
+
|
|
297
|
+
if (!rules || rules.length === 0) return '';
|
|
298
|
+
|
|
299
|
+
return rules.map(r => r.markdownRaw).join('\n\n---\n\n');
|
|
300
|
+
} catch (err) {
|
|
301
|
+
console.error('Error fetching global rules:', err);
|
|
302
|
+
return '';
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function renameSession(chatId, newLabel) {
|
|
307
|
+
if (!newLabel || !newLabel.trim()) return { success: false, message: 'Label cannot be empty' };
|
|
308
|
+
|
|
309
|
+
const slug = `agent-session-${chatId}`;
|
|
310
|
+
const config = await Markdown.model('JsonConfig').findOne({ $or: [{ slug }, { alias: slug }] });
|
|
311
|
+
|
|
312
|
+
if (!config) return { success: false, message: 'Session not found' };
|
|
313
|
+
|
|
314
|
+
const current = JSON.parse(config.jsonRaw);
|
|
315
|
+
current.label = newLabel.trim();
|
|
316
|
+
|
|
317
|
+
await jsonConfigsService.updateJsonConfig(config._id, { jsonRaw: JSON.stringify(current) });
|
|
318
|
+
|
|
319
|
+
return { success: true, label: newLabel.trim() };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function compactSession(agentId, chatId) {
|
|
323
|
+
const agent = await Agent.findById(agentId);
|
|
324
|
+
if (!agent) throw new Error('Agent not found');
|
|
325
|
+
|
|
326
|
+
const historyKey = `${agentId}:${chatId}`;
|
|
327
|
+
let history = await agentHistoryService.getHistory(agentId, chatId, MAX_HISTORY);
|
|
328
|
+
|
|
329
|
+
const sessionMetadata = await getOrCreateSession(agentId, chatId);
|
|
330
|
+
|
|
331
|
+
if (history.length === 0) {
|
|
332
|
+
const agentPrefix = agent.name.toLowerCase().replace(/[^a-z0-9]/g, '_');
|
|
333
|
+
const snapshotGroupCode = `${agentPrefix}__snapshots__${chatId}`;
|
|
334
|
+
const existingSnapshot = await Markdown.findOne({
|
|
335
|
+
category: 'agents_memory',
|
|
336
|
+
group_code: snapshotGroupCode
|
|
337
|
+
}).sort({ createdAt: -1 }).lean();
|
|
338
|
+
|
|
339
|
+
if (existingSnapshot) {
|
|
340
|
+
logger.log(`[agent.service] Manual compaction triggered for session ${chatId} (using existing snapshot)`);
|
|
341
|
+
history = [{ role: 'system', content: existingSnapshot.markdownRaw }];
|
|
342
|
+
} else if (sessionMetadata.lastSnapshotId) {
|
|
343
|
+
return { success: false, message: 'This session was previously compacted. No additional history to compact.' };
|
|
344
|
+
} else if (sessionMetadata.totalTokens > 0) {
|
|
345
|
+
return { success: false, message: 'Session history has expired from cache. Use /new to start a fresh session.' };
|
|
346
|
+
} else {
|
|
347
|
+
return { success: false, message: 'History is empty and no existing snapshot found for this session' };
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
logger.log(`[agent.service] Manual compaction triggered for session ${chatId}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const snapshot = await generateSnapshot(agent, chatId, history);
|
|
354
|
+
|
|
355
|
+
await updateSessionMetadata(chatId, {
|
|
356
|
+
lastSnapshotId: snapshot.slug,
|
|
357
|
+
totalTokens: 0
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
history = [{
|
|
361
|
+
role: 'assistant',
|
|
362
|
+
content: `Conversation summary at T=${new Date().toISOString()}`
|
|
363
|
+
}];
|
|
364
|
+
|
|
365
|
+
await agentHistoryService.deleteHistory(agentId, chatId);
|
|
366
|
+
await agentHistoryService.appendMessages(agentId, chatId, history);
|
|
367
|
+
|
|
368
|
+
return { success: true, snapshotId: snapshot.slug };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Process a message through the agent gateway
|
|
373
|
+
*/
|
|
374
|
+
async function processMessage(agentId, { content, senderId, chatId: inputChatId, metadata = {} }, options = {}) {
|
|
375
|
+
try {
|
|
376
|
+
const { abortSignal, onProgress } = options;
|
|
377
|
+
const agent = await Agent.findById(agentId);
|
|
378
|
+
if (!agent) throw new Error('Agent not found');
|
|
379
|
+
|
|
380
|
+
const chatId = inputChatId || crypto.randomUUID();
|
|
381
|
+
|
|
382
|
+
if (onProgress) onProgress({ status: 'initializing', message: 'Checking memory...' });
|
|
383
|
+
await ensureAgentMemory(agent);
|
|
384
|
+
await getOrCreateSession(agentId, chatId);
|
|
385
|
+
|
|
386
|
+
if (onProgress) onProgress({ status: 'initializing', message: 'Loading context...' });
|
|
387
|
+
const contextLength = await llmService.getModelContextLength(agent.model, agent.providerKey);
|
|
388
|
+
const systemPrompt = await getSystemPrompt(agent, chatId);
|
|
389
|
+
|
|
390
|
+
const historyKey = `${agentId}:${chatId}`;
|
|
391
|
+
let history = await agentHistoryService.getHistory(agentId, chatId, MAX_HISTORY);
|
|
392
|
+
const newMessages = [];
|
|
393
|
+
|
|
394
|
+
const userMsg = { role: 'user', content };
|
|
395
|
+
history.push(userMsg);
|
|
396
|
+
newMessages.push(userMsg);
|
|
397
|
+
|
|
398
|
+
if (history.length > MAX_HISTORY) {
|
|
399
|
+
history = history.slice(-MAX_HISTORY);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let iterations = 0;
|
|
403
|
+
const maxIterations = agent.maxIterations || 10;
|
|
404
|
+
let assistantContent = '';
|
|
405
|
+
let lastUsage = null;
|
|
406
|
+
|
|
407
|
+
while (iterations < maxIterations) {
|
|
408
|
+
if (abortSignal && abortSignal.aborted) {
|
|
409
|
+
throw new Error('Operation aborted');
|
|
410
|
+
}
|
|
411
|
+
iterations++;
|
|
412
|
+
|
|
413
|
+
if (onProgress) onProgress({ status: 'thinking', iteration: iterations, maxIterations });
|
|
414
|
+
|
|
415
|
+
const messages = [
|
|
416
|
+
{ role: 'system', content: systemPrompt },
|
|
417
|
+
...history
|
|
418
|
+
];
|
|
419
|
+
|
|
420
|
+
const isLastChance = iterations === maxIterations;
|
|
421
|
+
|
|
422
|
+
const tools = isLastChance ? [] : agentTools.getToolDefinitions();
|
|
423
|
+
|
|
424
|
+
let runtimeOptions = {
|
|
425
|
+
temperature: agent.temperature,
|
|
426
|
+
tools
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const response = await llmService.streamAdhoc({
|
|
430
|
+
providerKey: agent.providerKey,
|
|
431
|
+
model: agent.model,
|
|
432
|
+
messages: isLastChance
|
|
433
|
+
? [
|
|
434
|
+
...messages,
|
|
435
|
+
{
|
|
436
|
+
role: 'system',
|
|
437
|
+
content: 'IMPORTANT: This is your last turn. Provide a final answer now. DO NOT call any more tools.'
|
|
438
|
+
}
|
|
439
|
+
]
|
|
440
|
+
: messages
|
|
441
|
+
}, runtimeOptions, {
|
|
442
|
+
onToken: (token) => {
|
|
443
|
+
if (onProgress) onProgress({ status: 'streaming_content', token, iteration: iterations });
|
|
444
|
+
},
|
|
445
|
+
onReasoning: (reasoning) => {
|
|
446
|
+
if (onProgress) onProgress({ status: 'reasoning', token: reasoning, iteration: iterations });
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const { content: text, toolCalls, usage } = response;
|
|
451
|
+
if (usage) lastUsage = usage;
|
|
452
|
+
|
|
453
|
+
if (toolCalls && toolCalls.length > 0 && !isLastChance) {
|
|
454
|
+
if (onProgress) onProgress({ status: 'executing_tools', toolCalls, iteration: iterations });
|
|
455
|
+
|
|
456
|
+
const assistantMsg = {
|
|
457
|
+
role: 'assistant',
|
|
458
|
+
content: text || null,
|
|
459
|
+
tool_calls: toolCalls
|
|
460
|
+
};
|
|
461
|
+
history.push(assistantMsg);
|
|
462
|
+
newMessages.push(assistantMsg);
|
|
463
|
+
|
|
464
|
+
for (const toolCall of toolCalls) {
|
|
465
|
+
if (abortSignal && abortSignal.aborted) {
|
|
466
|
+
throw new Error('Operation aborted during tool execution');
|
|
467
|
+
}
|
|
468
|
+
const { name, arguments: argsString } = toolCall.function;
|
|
469
|
+
let args = {};
|
|
470
|
+
try {
|
|
471
|
+
args = JSON.parse(argsString);
|
|
472
|
+
} catch (e) {
|
|
473
|
+
console.error('Failed to parse tool arguments:', argsString);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (onProgress) onProgress({ status: 'executing_tool', tool: name, args, iteration: iterations });
|
|
477
|
+
const result = await agentTools.executeTool(name, args, { agent });
|
|
478
|
+
|
|
479
|
+
let isError = false;
|
|
480
|
+
try {
|
|
481
|
+
const parsed = JSON.parse(result);
|
|
482
|
+
if (parsed && parsed.error) isError = true;
|
|
483
|
+
} catch (e) {
|
|
484
|
+
isError = true;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const toolMsg = {
|
|
488
|
+
role: 'tool',
|
|
489
|
+
tool_call_id: toolCall.id,
|
|
490
|
+
content: result
|
|
491
|
+
};
|
|
492
|
+
history.push(toolMsg);
|
|
493
|
+
newMessages.push(toolMsg);
|
|
494
|
+
|
|
495
|
+
if (isError) {
|
|
496
|
+
const sysMsg = {
|
|
497
|
+
role: 'system',
|
|
498
|
+
content: 'IMPORTANT: The tool returned an error. Provide a friendly conversational response about this error.'
|
|
499
|
+
};
|
|
500
|
+
history.push(sysMsg);
|
|
501
|
+
newMessages.push(sysMsg);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
assistantContent = text;
|
|
506
|
+
const finalMsg = { role: 'assistant', content: assistantContent };
|
|
507
|
+
history.push(finalMsg);
|
|
508
|
+
newMessages.push(finalMsg);
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (onProgress) onProgress({ status: 'finalizing' });
|
|
514
|
+
if (lastUsage) {
|
|
515
|
+
const currentTokens = lastUsage.total_tokens || (lastUsage.prompt_tokens + lastUsage.completion_tokens);
|
|
516
|
+
await updateSessionMetadata(chatId, { totalTokens: currentTokens });
|
|
517
|
+
|
|
518
|
+
if (currentTokens / contextLength > COMPACTION_THRESHOLD) {
|
|
519
|
+
await compactSession(agentId, chatId);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
await agentHistoryService.appendMessages(agentId, chatId, newMessages);
|
|
524
|
+
|
|
525
|
+
const finalResponse = {
|
|
526
|
+
text: assistantContent || 'I processed your request but have no specific response.',
|
|
527
|
+
usage: lastUsage,
|
|
528
|
+
chatId
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
return finalResponse;
|
|
532
|
+
} catch (err) {
|
|
533
|
+
if (err.message !== 'Operation aborted' && !err.message.includes('aborted')) {
|
|
534
|
+
logger.error('Agent processMessage error:', err);
|
|
535
|
+
}
|
|
536
|
+
throw err;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
module.exports = {
|
|
541
|
+
processMessage,
|
|
542
|
+
getSystemPrompt,
|
|
543
|
+
getGlobalRules,
|
|
544
|
+
compactSession,
|
|
545
|
+
renameSession
|
|
546
|
+
};
|