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
package/server/routes/memory.js
CHANGED
|
@@ -186,7 +186,9 @@ router.get('/conversations', (req, res) => {
|
|
|
186
186
|
|
|
187
187
|
router.post('/conversations/search', (req, res) => {
|
|
188
188
|
const mm = req.app.locals.memoryManager;
|
|
189
|
-
const results = mm.searchConversations(req.session.userId, req.body.query
|
|
189
|
+
const results = mm.searchConversations(req.session.userId, req.body.query, {
|
|
190
|
+
sessions: parseInt(req.body.limit) || 8
|
|
191
|
+
});
|
|
190
192
|
res.json(results);
|
|
191
193
|
});
|
|
192
194
|
|
|
@@ -5,6 +5,7 @@ const db = require('../db/database');
|
|
|
5
5
|
const { requireAuth } = require('../middleware/auth');
|
|
6
6
|
const { normalizeWhatsAppWhitelist } = require('../utils/whatsapp');
|
|
7
7
|
const { UPDATE_STATUS_FILE, APP_DIR } = require('../../runtime/paths');
|
|
8
|
+
const { ensureDefaultAiSettings, DEFAULT_AI_SETTINGS } = require('../services/ai/settings');
|
|
8
9
|
|
|
9
10
|
router.use(requireAuth);
|
|
10
11
|
|
|
@@ -35,8 +36,9 @@ router.get('/meta/models', (req, res) => {
|
|
|
35
36
|
|
|
36
37
|
// Get all settings
|
|
37
38
|
router.get('/', (req, res) => {
|
|
39
|
+
ensureDefaultAiSettings(req.session.userId);
|
|
38
40
|
const rows = db.prepare('SELECT key, value FROM user_settings WHERE user_id = ?').all(req.session.userId);
|
|
39
|
-
const settings = {};
|
|
41
|
+
const settings = { ...DEFAULT_AI_SETTINGS };
|
|
40
42
|
for (const row of rows) {
|
|
41
43
|
try {
|
|
42
44
|
settings[row.key] = JSON.parse(row.value);
|
|
@@ -123,6 +125,31 @@ router.get('/token-usage/summary', (req, res) => {
|
|
|
123
125
|
return acc;
|
|
124
126
|
}, { tokens: 0, runs: 0 });
|
|
125
127
|
|
|
128
|
+
const promptMetricRows = db.prepare(`
|
|
129
|
+
SELECT prompt_metrics
|
|
130
|
+
FROM agent_runs
|
|
131
|
+
WHERE user_id = ? AND prompt_metrics IS NOT NULL
|
|
132
|
+
ORDER BY created_at DESC
|
|
133
|
+
LIMIT 20
|
|
134
|
+
`).all(userId);
|
|
135
|
+
|
|
136
|
+
const parsedMetrics = promptMetricRows
|
|
137
|
+
.map((row) => {
|
|
138
|
+
try { return JSON.parse(row.prompt_metrics); } catch { return null; }
|
|
139
|
+
})
|
|
140
|
+
.filter(Boolean);
|
|
141
|
+
|
|
142
|
+
const metricTotals = parsedMetrics.reduce((acc, item) => {
|
|
143
|
+
const last = item.lastEstimate || {};
|
|
144
|
+
acc.count += 1;
|
|
145
|
+
acc.system += Number(last.systemPromptTokens || 0);
|
|
146
|
+
acc.tools += Number(last.toolSchemaTokens || 0);
|
|
147
|
+
acc.history += Number(last.historyTokens || 0);
|
|
148
|
+
acc.recall += Number(last.recalledMemoryTokens || 0);
|
|
149
|
+
acc.replay += Number(last.toolReplayTokens || 0);
|
|
150
|
+
return acc;
|
|
151
|
+
}, { count: 0, system: 0, tools: 0, history: 0, recall: 0, replay: 0 });
|
|
152
|
+
|
|
126
153
|
res.json({
|
|
127
154
|
totals: {
|
|
128
155
|
totalTokens: Number(totals?.totalTokens || 0),
|
|
@@ -131,7 +158,18 @@ router.get('/token-usage/summary', (req, res) => {
|
|
|
131
158
|
last7DaysTokens: last7Totals.tokens,
|
|
132
159
|
last7DaysRuns: last7Totals.runs
|
|
133
160
|
},
|
|
134
|
-
last7Days
|
|
161
|
+
last7Days,
|
|
162
|
+
promptMetrics: {
|
|
163
|
+
sampleCount: metricTotals.count,
|
|
164
|
+
average: metricTotals.count === 0 ? null : {
|
|
165
|
+
systemPromptTokens: Math.round(metricTotals.system / metricTotals.count),
|
|
166
|
+
toolSchemaTokens: Math.round(metricTotals.tools / metricTotals.count),
|
|
167
|
+
historyTokens: Math.round(metricTotals.history / metricTotals.count),
|
|
168
|
+
recalledMemoryTokens: Math.round(metricTotals.recall / metricTotals.count),
|
|
169
|
+
toolReplayTokens: Math.round(metricTotals.replay / metricTotals.count)
|
|
170
|
+
},
|
|
171
|
+
latest: parsedMetrics[0] || null
|
|
172
|
+
}
|
|
135
173
|
});
|
|
136
174
|
});
|
|
137
175
|
|
package/server/routes/skills.js
CHANGED
|
@@ -1,108 +1,147 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const router = express.Router();
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
3
|
const { requireAuth } = require('../middleware/auth');
|
|
6
|
-
const {
|
|
7
|
-
|
|
8
|
-
const SKILLS_DIR = path.join(AGENT_DATA_DIR, 'skills');
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Resolve a client-supplied filename to an absolute path inside SKILLS_DIR.
|
|
12
|
-
* Returns null if the resolved path escapes the directory (path traversal attempt).
|
|
13
|
-
*/
|
|
14
|
-
function safeSkillPath(filename) {
|
|
15
|
-
if (!filename || typeof filename !== 'string') return null;
|
|
16
|
-
// Strip any directory components – only the basename is allowed
|
|
17
|
-
const base = path.basename(filename);
|
|
18
|
-
if (!base || base === '.' || base === '..') return null;
|
|
19
|
-
const resolved = path.resolve(SKILLS_DIR, base);
|
|
20
|
-
// Ensure the resolved path stays inside SKILLS_DIR
|
|
21
|
-
if (!resolved.startsWith(SKILLS_DIR + path.sep) && resolved !== SKILLS_DIR) return null;
|
|
22
|
-
return resolved;
|
|
23
|
-
}
|
|
4
|
+
const { sanitizeError } = require('../utils/security');
|
|
5
|
+
const { SkillRunner } = require('../services/ai/toolRunner');
|
|
24
6
|
|
|
25
7
|
router.use(requireAuth);
|
|
26
8
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
9
|
+
async function getSkillRunner(app) {
|
|
10
|
+
if (app.locals?.skillRunner) return app.locals.skillRunner;
|
|
11
|
+
const runner = new SkillRunner();
|
|
12
|
+
await runner.loadSkills();
|
|
13
|
+
return runner;
|
|
14
|
+
}
|
|
30
15
|
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
const meta = parseSkillMeta(content);
|
|
16
|
+
function parseSkillDocument(content) {
|
|
17
|
+
const match = String(content || '').match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
18
|
+
if (!match) {
|
|
35
19
|
return {
|
|
36
|
-
|
|
37
|
-
name: meta.name || f.replace('.md', ''),
|
|
38
|
-
description: meta.description || '',
|
|
39
|
-
trigger: meta.trigger || '',
|
|
40
|
-
enabled: meta.enabled !== false,
|
|
41
|
-
category: meta.category || 'general'
|
|
20
|
+
error: 'Skill files must start with frontmatter delimited by ---'
|
|
42
21
|
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
res.json(skills);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// Get a specific skill
|
|
49
|
-
router.get('/:filename', (req, res) => {
|
|
50
|
-
const fp = safeSkillPath(req.params.filename);
|
|
51
|
-
if (!fp || !fs.existsSync(fp)) return res.status(404).json({ error: 'Skill not found' });
|
|
52
|
-
|
|
53
|
-
const content = fs.readFileSync(fp, 'utf-8');
|
|
54
|
-
res.json({ filename: req.params.filename, content, meta: parseSkillMeta(content) });
|
|
55
|
-
});
|
|
22
|
+
}
|
|
56
23
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
24
|
+
const frontmatter = {};
|
|
25
|
+
for (const line of match[1].split('\n')) {
|
|
26
|
+
const colon = line.indexOf(':');
|
|
27
|
+
if (colon === -1) continue;
|
|
28
|
+
const key = line.slice(0, colon).trim();
|
|
29
|
+
let value = line.slice(colon + 1).trim();
|
|
30
|
+
if (value === 'true') value = true;
|
|
31
|
+
else if (value === 'false') value = false;
|
|
32
|
+
else if ((value.startsWith('{') && value.endsWith('}')) || (value.startsWith('[') && value.endsWith(']'))) {
|
|
33
|
+
try { value = JSON.parse(value); } catch { /* keep raw */ }
|
|
34
|
+
}
|
|
35
|
+
frontmatter[key] = value;
|
|
36
|
+
}
|
|
61
37
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
38
|
+
if (!frontmatter.name) {
|
|
39
|
+
return { error: 'Skill frontmatter must include name' };
|
|
40
|
+
}
|
|
65
41
|
|
|
66
|
-
|
|
42
|
+
return {
|
|
43
|
+
name: String(frontmatter.name),
|
|
44
|
+
description: String(frontmatter.description || ''),
|
|
45
|
+
instructions: match[2].trim(),
|
|
46
|
+
metadata: Object.fromEntries(
|
|
47
|
+
Object.entries(frontmatter).filter(([key]) => !['name', 'description'].includes(key))
|
|
48
|
+
)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
67
51
|
|
|
68
|
-
|
|
69
|
-
|
|
52
|
+
router.get('/', async (req, res) => {
|
|
53
|
+
try {
|
|
54
|
+
const runner = await getSkillRunner(req.app);
|
|
55
|
+
const skills = runner.getAll().map((skill) => ({
|
|
56
|
+
name: skill.name,
|
|
57
|
+
description: skill.description,
|
|
58
|
+
enabled: skill.enabled,
|
|
59
|
+
draft: skill.metadata?.draft === true,
|
|
60
|
+
category: skill.metadata?.category || 'general',
|
|
61
|
+
trigger: skill.metadata?.trigger || '',
|
|
62
|
+
source: skill.metadata?.source || 'local',
|
|
63
|
+
autoCreated: skill.metadata?.auto_created === true,
|
|
64
|
+
filePath: skill.filePath
|
|
65
|
+
}));
|
|
66
|
+
res.json(skills.sort((a, b) => {
|
|
67
|
+
if (a.draft !== b.draft) return a.draft ? -1 : 1;
|
|
68
|
+
return a.name.localeCompare(b.name);
|
|
69
|
+
}));
|
|
70
|
+
} catch (err) {
|
|
71
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
72
|
+
}
|
|
70
73
|
});
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
router.get('/:name', async (req, res) => {
|
|
76
|
+
try {
|
|
77
|
+
const runner = await getSkillRunner(req.app);
|
|
78
|
+
const skill = runner.getSkill(req.params.name);
|
|
79
|
+
if (!skill) return res.status(404).json({ error: 'Skill not found' });
|
|
80
|
+
const fs = require('fs');
|
|
81
|
+
const content = fs.readFileSync(skill.filePath, 'utf-8');
|
|
82
|
+
res.json({
|
|
83
|
+
name: skill.name,
|
|
84
|
+
content,
|
|
85
|
+
meta: skill.metadata,
|
|
86
|
+
enabled: skill.metadata?.enabled !== false
|
|
87
|
+
});
|
|
88
|
+
} catch (err) {
|
|
89
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
90
|
+
}
|
|
79
91
|
});
|
|
80
92
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
router.post('/', async (req, res) => {
|
|
94
|
+
try {
|
|
95
|
+
const runner = await getSkillRunner(req.app);
|
|
96
|
+
const parsed = parseSkillDocument(req.body.content);
|
|
97
|
+
if (parsed.error) return res.status(400).json({ error: parsed.error });
|
|
98
|
+
|
|
99
|
+
const result = runner.createSkill(
|
|
100
|
+
req.body.filename || parsed.name,
|
|
101
|
+
parsed.description,
|
|
102
|
+
parsed.instructions,
|
|
103
|
+
parsed.metadata
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (result.error) return res.status(400).json(result);
|
|
107
|
+
res.status(201).json(result);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
110
|
+
}
|
|
88
111
|
});
|
|
89
112
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
113
|
+
router.put('/:name', async (req, res) => {
|
|
114
|
+
try {
|
|
115
|
+
const runner = await getSkillRunner(req.app);
|
|
116
|
+
if (typeof req.body.enabled === 'boolean' && !req.body.content) {
|
|
117
|
+
const result = runner.setSkillEnabled(req.params.name, req.body.enabled);
|
|
118
|
+
if (result.error) return res.status(404).json(result);
|
|
119
|
+
return res.json(result);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const parsed = parseSkillDocument(req.body.content);
|
|
123
|
+
if (parsed.error) return res.status(400).json({ error: parsed.error });
|
|
124
|
+
const result = runner.updateSkill(req.params.name, {
|
|
125
|
+
description: parsed.description,
|
|
126
|
+
instructions: parsed.instructions,
|
|
127
|
+
metadata: parsed.metadata
|
|
128
|
+
});
|
|
129
|
+
if (result.error) return res.status(404).json(result);
|
|
130
|
+
res.json(result);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
94
135
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
meta[key] = val;
|
|
136
|
+
router.delete('/:name', async (req, res) => {
|
|
137
|
+
try {
|
|
138
|
+
const runner = await getSkillRunner(req.app);
|
|
139
|
+
const result = runner.deleteSkill(req.params.name);
|
|
140
|
+
if (result.error) return res.status(404).json(result);
|
|
141
|
+
res.json(result);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
104
144
|
}
|
|
105
|
-
|
|
106
|
-
}
|
|
145
|
+
});
|
|
107
146
|
|
|
108
147
|
module.exports = router;
|
package/server/routes/store.js
CHANGED
|
@@ -317,6 +317,46 @@ Run in the user's specified directory (default: current working directory):
|
|
|
317
317
|
Present as a structured git dashboard. Note any uncommitted changes or detached HEAD.`
|
|
318
318
|
},
|
|
319
319
|
|
|
320
|
+
{
|
|
321
|
+
id: 'pdf-toolkit',
|
|
322
|
+
name: 'PDF Toolkit',
|
|
323
|
+
description: 'Inspect, extract, split, merge, compress, and rearrange PDF files.',
|
|
324
|
+
category: 'productivity',
|
|
325
|
+
icon: '📄',
|
|
326
|
+
content: `---
|
|
327
|
+
name: pdf-toolkit
|
|
328
|
+
description: Inspect, extract, split, merge, compress, and rearrange PDF files
|
|
329
|
+
category: productivity
|
|
330
|
+
icon: 📄
|
|
331
|
+
enabled: true
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
Work with PDF files using whatever is available on the machine. Prefer tools in this order when relevant:
|
|
335
|
+
- \`pdfinfo\`, \`pdftotext\`, \`pdftoppm\`, \`pdftocairo\` from Poppler for inspection, text extraction, and page rendering
|
|
336
|
+
- \`qpdf\` for splitting, merging, rotating, decrypting, and page selection
|
|
337
|
+
- \`pdftk\` for merge/split/stamp workflows if available
|
|
338
|
+
- \`mutool\` for extracting text, objects, and cleaning PDFs
|
|
339
|
+
- \`gs\` (Ghostscript) for compression or PDF regeneration
|
|
340
|
+
|
|
341
|
+
Workflow:
|
|
342
|
+
1. Verify the input file exists.
|
|
343
|
+
2. Check which PDF utilities are installed with \`which\`.
|
|
344
|
+
3. Choose the safest tool for the requested operation.
|
|
345
|
+
4. Write outputs next to the source file unless the user specifies a destination.
|
|
346
|
+
5. Report the output path, page counts, and any limitations clearly.
|
|
347
|
+
|
|
348
|
+
Common tasks:
|
|
349
|
+
- Inspect a PDF with \`pdfinfo <file.pdf>\`
|
|
350
|
+
- Extract text with \`pdftotext <file.pdf> -\` or \`pdftotext -layout <file.pdf> -\`
|
|
351
|
+
- Merge PDFs with \`qpdf --empty --pages a.pdf b.pdf -- out.pdf\`
|
|
352
|
+
- Split pages with \`qpdf in.pdf --pages in.pdf 1-5 -- out.pdf\`
|
|
353
|
+
- Reorder or remove pages with \`qpdf in.pdf --pages in.pdf 1,3,5-7 -- out.pdf\`
|
|
354
|
+
- Compress via Ghostscript:
|
|
355
|
+
\`gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/ebook -dNOPAUSE -dQUIET -dBATCH -sOutputFile=out.pdf in.pdf\`
|
|
356
|
+
|
|
357
|
+
Preserve the original file unless the user explicitly asks to overwrite it.`
|
|
358
|
+
},
|
|
359
|
+
|
|
320
360
|
{
|
|
321
361
|
id: 'docker-status',
|
|
322
362
|
name: 'Docker Status',
|
|
@@ -1047,6 +1087,66 @@ ruby <file>.rb # Ruby
|
|
|
1047
1087
|
- No unused imports or dead code left behind`
|
|
1048
1088
|
},
|
|
1049
1089
|
|
|
1090
|
+
{
|
|
1091
|
+
id: 'csv-toolkit',
|
|
1092
|
+
name: 'CSV Toolkit',
|
|
1093
|
+
description: 'Inspect, clean, summarize, filter, and transform CSV and TSV files.',
|
|
1094
|
+
category: 'productivity',
|
|
1095
|
+
icon: '📊',
|
|
1096
|
+
content: `---
|
|
1097
|
+
name: csv-toolkit
|
|
1098
|
+
description: Inspect, clean, summarize, filter, and transform CSV and TSV files
|
|
1099
|
+
category: productivity
|
|
1100
|
+
icon: 📊
|
|
1101
|
+
enabled: true
|
|
1102
|
+
---
|
|
1103
|
+
|
|
1104
|
+
Work with local CSV or TSV data using lightweight shell tools.
|
|
1105
|
+
|
|
1106
|
+
Preferred tools:
|
|
1107
|
+
- \`python3\` with the standard \`csv\` module for reliable parsing
|
|
1108
|
+
- \`mlr\` (Miller) if installed for fast tabular summaries
|
|
1109
|
+
- \`csvkit\` commands such as \`csvlook\`, \`csvcut\`, \`csvstat\`, \`csvgrep\` if installed
|
|
1110
|
+
|
|
1111
|
+
Workflow:
|
|
1112
|
+
1. Detect delimiter and header structure from the file.
|
|
1113
|
+
2. For quick inspection, show row count, column count, header names, and a 5-row preview.
|
|
1114
|
+
3. For analysis requests, compute exactly what the user asked for: filtering, grouping, aggregations, sorting, or conversion to JSON or Markdown.
|
|
1115
|
+
4. Save transformed output to a sibling file unless the user asked for inline output only.
|
|
1116
|
+
|
|
1117
|
+
Avoid naive comma-splitting because quoted fields can break it. If the file is large, sample first and then run the full transform once the operation is clear.`
|
|
1118
|
+
},
|
|
1119
|
+
|
|
1120
|
+
{
|
|
1121
|
+
id: 'markdown-workbench',
|
|
1122
|
+
name: 'Markdown Workbench',
|
|
1123
|
+
description: 'Format, lint, outline, and convert Markdown notes and docs.',
|
|
1124
|
+
category: 'productivity',
|
|
1125
|
+
icon: '📝',
|
|
1126
|
+
content: `---
|
|
1127
|
+
name: markdown-workbench
|
|
1128
|
+
description: Format, lint, outline, and convert Markdown notes and docs
|
|
1129
|
+
category: productivity
|
|
1130
|
+
icon: 📝
|
|
1131
|
+
enabled: true
|
|
1132
|
+
---
|
|
1133
|
+
|
|
1134
|
+
Help turn rough notes into usable docs quickly.
|
|
1135
|
+
|
|
1136
|
+
Workflow:
|
|
1137
|
+
1. Read the source Markdown or note file.
|
|
1138
|
+
2. Decide whether the user needs cleanup and formatting, heading structure, summary or outline, checklist extraction, or conversion to HTML or PDF.
|
|
1139
|
+
3. Preserve meaning while improving structure and readability.
|
|
1140
|
+
|
|
1141
|
+
Common tasks:
|
|
1142
|
+
- Normalize headings, lists, code fences, and spacing
|
|
1143
|
+
- Pull out tasks into a checklist grouped by topic or urgency
|
|
1144
|
+
- Build a short linked outline from headings
|
|
1145
|
+
- If available, use \`pandoc\` to convert Markdown to HTML, DOCX, or PDF
|
|
1146
|
+
|
|
1147
|
+
Prefer preserving the user's tone unless they ask for a rewrite.`
|
|
1148
|
+
},
|
|
1149
|
+
|
|
1050
1150
|
// ── MAKER ────────────────────────────────────────────────────────────────────
|
|
1051
1151
|
{
|
|
1052
1152
|
id: 'psa-car-controller',
|
|
@@ -2,50 +2,38 @@ async function compact(messages, provider, model) {
|
|
|
2
2
|
const systemMsg = messages.find(m => m.role === 'system');
|
|
3
3
|
const nonSystem = messages.filter(m => m.role !== 'system');
|
|
4
4
|
|
|
5
|
-
// Only compact once history is clearly old enough to avoid touching recent context.
|
|
6
5
|
if (nonSystem.length < 12) return messages;
|
|
7
6
|
|
|
8
|
-
const keepRecent =
|
|
7
|
+
const keepRecent = 8;
|
|
9
8
|
const toCompact = nonSystem.slice(0, -keepRecent);
|
|
10
9
|
const recent = nonSystem.slice(-keepRecent);
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} else if (msg.role === 'assistant') {
|
|
17
|
-
const content = (msg.content || '').slice(0, 500);
|
|
18
|
-
if (msg.tool_calls) {
|
|
19
|
-
const tools = msg.tool_calls.map(tc => tc.function.name).join(', ');
|
|
20
|
-
compactionText += `Assistant: [used tools: ${tools}] ${content}\n`;
|
|
21
|
-
} else {
|
|
22
|
-
compactionText += `Assistant: ${content}\n`;
|
|
23
|
-
}
|
|
24
|
-
} else if (msg.role === 'tool') {
|
|
25
|
-
const result = (msg.content || '').slice(0, 200);
|
|
26
|
-
compactionText += `Tool result: ${result}\n`;
|
|
11
|
+
const compactionText = toCompact.map((msg) => {
|
|
12
|
+
if (msg.role === 'assistant' && msg.tool_calls) {
|
|
13
|
+
const tools = msg.tool_calls.map((tc) => tc.function.name).join(', ');
|
|
14
|
+
return `assistant(tools:${tools}) ${(msg.content || '').slice(0, 320)}`;
|
|
27
15
|
}
|
|
28
|
-
|
|
16
|
+
if (msg.role === 'tool') {
|
|
17
|
+
return `tool:${msg.name || 'tool'} ${(msg.content || '').slice(0, 220)}`;
|
|
18
|
+
}
|
|
19
|
+
return `${msg.role}: ${(msg.content || '').slice(0, 360)}`;
|
|
20
|
+
}).join('\n');
|
|
29
21
|
|
|
30
22
|
const summaryPrompt = [
|
|
31
|
-
{ role: 'system', content: '
|
|
23
|
+
{ role: 'system', content: 'Compress conversation context. Preserve goals, constraints, tool outcomes, errors, and unresolved work. Keep it compact.' },
|
|
32
24
|
{ role: 'user', content: `Summarize this conversation:\n\n${compactionText}` }
|
|
33
25
|
];
|
|
34
26
|
|
|
35
27
|
try {
|
|
36
|
-
const response = await provider.chat(summaryPrompt, [], { model, maxTokens:
|
|
28
|
+
const response = await provider.chat(summaryPrompt, [], { model, maxTokens: 320 });
|
|
37
29
|
const summary = response.content || 'Previous conversation context (summary unavailable).';
|
|
38
30
|
|
|
39
31
|
const compactedMessages = [];
|
|
40
32
|
if (systemMsg) compactedMessages.push(systemMsg);
|
|
41
33
|
compactedMessages.push({
|
|
42
|
-
role: '
|
|
34
|
+
role: 'system',
|
|
43
35
|
content: `[Previous conversation summary]\n${summary}`
|
|
44
36
|
});
|
|
45
|
-
compactedMessages.push({
|
|
46
|
-
role: 'assistant',
|
|
47
|
-
content: 'Understood. I have the context from our previous conversation. Continuing.'
|
|
48
|
-
});
|
|
49
37
|
compactedMessages.push(...recent);
|
|
50
38
|
|
|
51
39
|
return compactedMessages;
|
|
@@ -54,13 +42,9 @@ async function compact(messages, provider, model) {
|
|
|
54
42
|
const trimmed = [];
|
|
55
43
|
if (systemMsg) trimmed.push(systemMsg);
|
|
56
44
|
trimmed.push({
|
|
57
|
-
role: '
|
|
45
|
+
role: 'system',
|
|
58
46
|
content: '[Earlier conversation context was trimmed due to length]'
|
|
59
47
|
});
|
|
60
|
-
trimmed.push({
|
|
61
|
-
role: 'assistant',
|
|
62
|
-
content: 'Understood. Some earlier context was trimmed. Continuing with recent messages.'
|
|
63
|
-
});
|
|
64
48
|
trimmed.push(...recent);
|
|
65
49
|
return trimmed;
|
|
66
50
|
}
|