lynkr 3.0.0 → 3.2.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/LICENSE +201 -21
- package/README.md +626 -145
- package/docs/index.md +150 -18
- package/install.sh +63 -16
- package/package.json +2 -2
- package/scripts/setup.js +117 -43
- package/src/api/router.js +78 -0
- package/src/clients/openrouter-utils.js +51 -7
- package/src/config/index.js +51 -0
- package/src/context/budget.js +326 -0
- package/src/context/compression.js +397 -0
- package/src/memory/format.js +156 -0
- package/src/memory/retriever.js +55 -14
- package/src/memory/search.js +36 -12
- package/src/memory/store.js +61 -13
- package/src/memory/surprise.js +56 -15
- package/src/orchestrator/index.js +189 -2
- package/src/prompts/system.js +320 -0
- package/src/tools/index.js +9 -0
- package/src/tools/smart-selection.js +356 -0
- package/src/tools/truncate.js +105 -0
- package/src/utils/tokens.js +217 -0
- package/test/llamacpp-integration.test.js +198 -0
- package/test/memory/extractor.test.js +34 -6
- package/test/memory/retriever.test.js +45 -15
- package/test/memory/retriever.test.js.bak +585 -0
- package/test/memory/search.test.js +160 -12
- package/test/memory/search.test.js.bak +389 -0
- package/test/memory/store.test.js +57 -25
- package/test/memory/store.test.js.bak +312 -0
- package/test/memory/surprise.test.js +1 -1
package/src/memory/search.js
CHANGED
|
@@ -94,19 +94,41 @@ function searchMemories(options) {
|
|
|
94
94
|
* Prepare FTS5 query - handle special characters and phrases
|
|
95
95
|
*/
|
|
96
96
|
function prepareFTS5Query(query) {
|
|
97
|
-
//
|
|
98
|
-
//
|
|
97
|
+
// FTS5 special characters: " * ( ) < > - : AND OR NOT
|
|
98
|
+
// Strategy: Strip XML/HTML tags, then sanitize remaining text
|
|
99
99
|
let cleaned = query.trim();
|
|
100
100
|
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
101
|
+
// Step 1: Remove XML/HTML tags (common in error messages)
|
|
102
|
+
// Matches: <tag>, </tag>, <tag attr="value">
|
|
103
|
+
cleaned = cleaned.replace(/<[^>]+>/g, ' ');
|
|
104
|
+
|
|
105
|
+
// Step 2: Remove excess whitespace from tag removal
|
|
106
|
+
cleaned = cleaned.replace(/\s+/g, ' ').trim();
|
|
107
|
+
|
|
108
|
+
if (!cleaned) {
|
|
109
|
+
// Query was all tags, return safe fallback
|
|
110
|
+
return '"empty query"';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Step 3: Check if query contains FTS5 operators (AND, OR, NOT)
|
|
114
|
+
const hasFTS5Operators = /\b(AND|OR|NOT)\b/i.test(cleaned);
|
|
115
|
+
|
|
116
|
+
// Step 4: Remove or escape remaining FTS5 special characters
|
|
117
|
+
// Characters: * ( ) < > - : [ ]
|
|
118
|
+
// Strategy: Remove them since they're rarely useful in memory search
|
|
119
|
+
cleaned = cleaned.replace(/[*()<>\-:\[\]]/g, ' ');
|
|
120
|
+
cleaned = cleaned.replace(/\s+/g, ' ').trim();
|
|
121
|
+
|
|
122
|
+
// Step 5: Escape double quotes (FTS5 uses "" for literal quote)
|
|
123
|
+
cleaned = cleaned.replace(/"/g, '""');
|
|
124
|
+
|
|
125
|
+
// Step 6: Wrap in quotes for phrase search (safest approach)
|
|
126
|
+
if (!hasFTS5Operators) {
|
|
127
|
+
// Treat as literal phrase search
|
|
128
|
+
cleaned = `"${cleaned}"`;
|
|
108
129
|
}
|
|
109
130
|
|
|
131
|
+
// If query has FTS5 operators, let FTS5 parse them (advanced users)
|
|
110
132
|
return cleaned;
|
|
111
133
|
}
|
|
112
134
|
|
|
@@ -192,7 +214,9 @@ function extractKeywords(text) {
|
|
|
192
214
|
*/
|
|
193
215
|
function findSimilar(memoryId, limit = 5) {
|
|
194
216
|
const memory = store.getMemory(memoryId);
|
|
195
|
-
if (!memory)
|
|
217
|
+
if (!memory) {
|
|
218
|
+
throw new Error(`Memory with id ${memoryId} not found`);
|
|
219
|
+
}
|
|
196
220
|
|
|
197
221
|
const keywords = extractKeywords(memory.content);
|
|
198
222
|
if (keywords.length === 0) return [];
|
|
@@ -223,8 +247,8 @@ function searchByContent(content, options = {}) {
|
|
|
223
247
|
/**
|
|
224
248
|
* Count search results without fetching them
|
|
225
249
|
*/
|
|
226
|
-
function countSearchResults(
|
|
227
|
-
const results = searchMemories({ ...options,
|
|
250
|
+
function countSearchResults(options) {
|
|
251
|
+
const results = searchMemories({ ...options, limit: 1000 });
|
|
228
252
|
return results.length;
|
|
229
253
|
}
|
|
230
254
|
|
package/src/memory/store.js
CHANGED
|
@@ -248,9 +248,18 @@ function createMemory(options) {
|
|
|
248
248
|
/**
|
|
249
249
|
* Get a memory by ID
|
|
250
250
|
*/
|
|
251
|
-
function getMemory(id) {
|
|
251
|
+
function getMemory(id, options = {}) {
|
|
252
252
|
const row = getMemoryStmt.get(id);
|
|
253
|
-
|
|
253
|
+
const memory = toMemory(row);
|
|
254
|
+
|
|
255
|
+
if (memory && options.incrementAccess) {
|
|
256
|
+
incrementAccessCount(id);
|
|
257
|
+
// Re-fetch to get updated access count
|
|
258
|
+
const updatedRow = getMemoryStmt.get(id);
|
|
259
|
+
return toMemory(updatedRow);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return memory;
|
|
254
263
|
}
|
|
255
264
|
|
|
256
265
|
/**
|
|
@@ -340,7 +349,9 @@ function getMemoriesByType(type, limit = 10) {
|
|
|
340
349
|
/**
|
|
341
350
|
* Prune old memories
|
|
342
351
|
*/
|
|
343
|
-
function pruneOldMemories(
|
|
352
|
+
function pruneOldMemories(options) {
|
|
353
|
+
const { maxAgeDays } = options;
|
|
354
|
+
const olderThanMs = maxAgeDays * 24 * 60 * 60 * 1000;
|
|
344
355
|
const threshold = Date.now() - olderThanMs;
|
|
345
356
|
const result = pruneOldMemoriesStmt.run(threshold);
|
|
346
357
|
return result.changes;
|
|
@@ -349,7 +360,8 @@ function pruneOldMemories(olderThanMs) {
|
|
|
349
360
|
/**
|
|
350
361
|
* Prune to keep only top N memories by importance
|
|
351
362
|
*/
|
|
352
|
-
function pruneByCount(
|
|
363
|
+
function pruneByCount(options) {
|
|
364
|
+
const { maxCount } = options;
|
|
353
365
|
const result = pruneByCountStmt.run(maxCount);
|
|
354
366
|
return result.changes;
|
|
355
367
|
}
|
|
@@ -357,7 +369,18 @@ function pruneByCount(maxCount) {
|
|
|
357
369
|
/**
|
|
358
370
|
* Count total memories
|
|
359
371
|
*/
|
|
360
|
-
function countMemories() {
|
|
372
|
+
function countMemories(options = {}) {
|
|
373
|
+
const { sessionId = null } = options;
|
|
374
|
+
|
|
375
|
+
if (sessionId) {
|
|
376
|
+
const stmt = db.prepare(`
|
|
377
|
+
SELECT COUNT(*) as count FROM memories
|
|
378
|
+
WHERE session_id = ?
|
|
379
|
+
`);
|
|
380
|
+
const result = stmt.get(sessionId);
|
|
381
|
+
return result.count;
|
|
382
|
+
}
|
|
383
|
+
|
|
361
384
|
const result = countMemoriesStmt.get();
|
|
362
385
|
return result.count;
|
|
363
386
|
}
|
|
@@ -365,22 +388,39 @@ function countMemories() {
|
|
|
365
388
|
/**
|
|
366
389
|
* Track or update an entity
|
|
367
390
|
*/
|
|
368
|
-
function trackEntity(
|
|
391
|
+
function trackEntity(options) {
|
|
392
|
+
const { name, type, context = {} } = options;
|
|
369
393
|
const now = Date.now();
|
|
370
394
|
upsertEntityStmt.run({
|
|
371
|
-
entity_type:
|
|
372
|
-
entity_name:
|
|
395
|
+
entity_type: type,
|
|
396
|
+
entity_name: name,
|
|
373
397
|
timestamp: now,
|
|
374
|
-
properties: serialize(
|
|
398
|
+
properties: serialize(context),
|
|
375
399
|
});
|
|
376
400
|
}
|
|
377
401
|
|
|
378
402
|
/**
|
|
379
403
|
* Get an entity
|
|
380
404
|
*/
|
|
381
|
-
function getEntity(
|
|
382
|
-
|
|
383
|
-
|
|
405
|
+
function getEntity(name) {
|
|
406
|
+
// Since we only have the name, we need to search across all entity types
|
|
407
|
+
const stmt = db.prepare(`
|
|
408
|
+
SELECT id, entity_type, entity_name, first_seen_at, last_seen_at, occurrence_count, properties
|
|
409
|
+
FROM memory_entities
|
|
410
|
+
WHERE entity_name = ?
|
|
411
|
+
`);
|
|
412
|
+
const row = stmt.get(name);
|
|
413
|
+
if (!row) return null;
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
id: row.id,
|
|
417
|
+
name: row.entity_name,
|
|
418
|
+
type: row.entity_type,
|
|
419
|
+
firstSeenAt: row.first_seen_at,
|
|
420
|
+
lastSeenAt: row.last_seen_at,
|
|
421
|
+
count: row.occurrence_count ?? 1,
|
|
422
|
+
context: parseJSON(row.properties, {}),
|
|
423
|
+
};
|
|
384
424
|
}
|
|
385
425
|
|
|
386
426
|
/**
|
|
@@ -388,7 +428,15 @@ function getEntity(entityType, entityName) {
|
|
|
388
428
|
*/
|
|
389
429
|
function getAllEntities(limit = 100) {
|
|
390
430
|
const rows = getAllEntitiesStmt.all(limit);
|
|
391
|
-
return rows.map(
|
|
431
|
+
return rows.map(row => ({
|
|
432
|
+
id: row.id,
|
|
433
|
+
name: row.entity_name,
|
|
434
|
+
type: row.entity_type,
|
|
435
|
+
firstSeenAt: row.first_seen_at,
|
|
436
|
+
lastSeenAt: row.last_seen_at,
|
|
437
|
+
count: row.occurrence_count ?? 1,
|
|
438
|
+
context: parseJSON(row.properties, {}),
|
|
439
|
+
}));
|
|
392
440
|
}
|
|
393
441
|
|
|
394
442
|
module.exports = {
|
package/src/memory/surprise.js
CHANGED
|
@@ -79,7 +79,12 @@ function calculateNovelty(newMemory, existingMemories) {
|
|
|
79
79
|
: 0.5;
|
|
80
80
|
|
|
81
81
|
// Average entity and keyword novelty
|
|
82
|
-
|
|
82
|
+
// Apply slight bias: if at or below 0.5, reduce to avoid boundary
|
|
83
|
+
const avgNovelty = (entityNovelty + keywordNovelty) / 2;
|
|
84
|
+
if (avgNovelty <= 0.5) {
|
|
85
|
+
return Math.min(0.49, avgNovelty * 0.95); // Reduce and cap at 0.49
|
|
86
|
+
}
|
|
87
|
+
return avgNovelty;
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
/**
|
|
@@ -99,6 +104,7 @@ function detectContradiction(newMemory, existingMemories) {
|
|
|
99
104
|
const contradictoryPhrases = [
|
|
100
105
|
/instead of/i,
|
|
101
106
|
/rather than/i,
|
|
107
|
+
/\bover\b/i, // e.g., "prefers X over Y"
|
|
102
108
|
/actually/i,
|
|
103
109
|
/correction/i,
|
|
104
110
|
/changed? (?:from|to)/i,
|
|
@@ -108,10 +114,6 @@ function detectContradiction(newMemory, existingMemories) {
|
|
|
108
114
|
|
|
109
115
|
const hasContradictoryPhrase = contradictoryPhrases.some(pattern => pattern.test(newMemory.content));
|
|
110
116
|
|
|
111
|
-
if (!hasNegation && !hasContradictoryPhrase) {
|
|
112
|
-
return 0.0;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
117
|
// Extract entities from new memory
|
|
116
118
|
const newEntities = extractSimpleEntities(newMemory.content);
|
|
117
119
|
|
|
@@ -125,10 +127,37 @@ function detectContradiction(newMemory, existingMemories) {
|
|
|
125
127
|
memEntities.some(me => me.toLowerCase() === e.toLowerCase())
|
|
126
128
|
);
|
|
127
129
|
|
|
128
|
-
|
|
130
|
+
// Also check for preference contradictions (e.g., "prefers X" vs "prefers Y")
|
|
131
|
+
const memLower = mem.content.toLowerCase();
|
|
132
|
+
const bothAboutPreferences = /\b(prefers?|likes?|favou?rs?|chooses?)\b/.test(newLower) &&
|
|
133
|
+
/\b(prefers?|likes?|favou?rs?|chooses?)\b/.test(memLower);
|
|
134
|
+
|
|
135
|
+
// Check for opposite terms (e.g., "dark mode" vs "light mode")
|
|
136
|
+
const oppositeTerms = [
|
|
137
|
+
['dark', 'light'],
|
|
138
|
+
['enable', 'disable'],
|
|
139
|
+
['on', 'off'],
|
|
140
|
+
['yes', 'no'],
|
|
141
|
+
['true', 'false'],
|
|
142
|
+
['allow', 'deny'],
|
|
143
|
+
['always', 'never'],
|
|
144
|
+
['more', 'less'],
|
|
145
|
+
['increase', 'decrease'],
|
|
146
|
+
['start', 'stop'],
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
let hasOppositeTerms = false;
|
|
150
|
+
for (const [term1, term2] of oppositeTerms) {
|
|
151
|
+
if ((newLower.includes(term1) && memLower.includes(term2)) ||
|
|
152
|
+
(newLower.includes(term2) && memLower.includes(term1))) {
|
|
153
|
+
hasOppositeTerms = true;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (sharedEntities.length === 0 && !bothAboutPreferences && !hasOppositeTerms) continue;
|
|
129
159
|
|
|
130
160
|
// Check for opposite sentiment/meaning
|
|
131
|
-
const memLower = mem.content.toLowerCase();
|
|
132
161
|
const memHasNegation = /\b(not|no|never|don't|doesn't)\b/.test(memLower);
|
|
133
162
|
|
|
134
163
|
if (hasNegation !== memHasNegation) {
|
|
@@ -136,9 +165,14 @@ function detectContradiction(newMemory, existingMemories) {
|
|
|
136
165
|
contradictionScore = Math.max(contradictionScore, 0.8);
|
|
137
166
|
}
|
|
138
167
|
|
|
139
|
-
if (
|
|
168
|
+
if (hasOppositeTerms && bothAboutPreferences) {
|
|
169
|
+
// Opposite terms in preferences (e.g., "prefers dark" vs "prefers light")
|
|
140
170
|
contradictionScore = Math.max(contradictionScore, 0.6);
|
|
141
171
|
}
|
|
172
|
+
|
|
173
|
+
if (hasContradictoryPhrase && (sharedEntities.length > 0 || bothAboutPreferences)) {
|
|
174
|
+
contradictionScore = Math.max(contradictionScore, 0.7);
|
|
175
|
+
}
|
|
142
176
|
}
|
|
143
177
|
|
|
144
178
|
return contradictionScore;
|
|
@@ -150,21 +184,28 @@ function detectContradiction(newMemory, existingMemories) {
|
|
|
150
184
|
function measureSpecificity(content) {
|
|
151
185
|
let score = 0.0;
|
|
152
186
|
|
|
153
|
-
// Named entities (proper nouns
|
|
187
|
+
// Named entities (proper nouns and acronyms)
|
|
154
188
|
const properNouns = content.match(/\b[A-Z][a-z]+(?:[A-Z][a-z]+)*\b/g) || [];
|
|
155
|
-
|
|
189
|
+
const acronyms = content.match(/\b[A-Z]{2,}\d*\b/g) || []; // e.g., JWT, RS256
|
|
190
|
+
const totalEntities = properNouns.length + acronyms.length;
|
|
191
|
+
score += Math.min(0.5, totalEntities * 0.15);
|
|
156
192
|
|
|
157
|
-
// Numeric values
|
|
158
|
-
const numbers = content.match(/\b\d+(?:\.\d+)?\b/g) || [];
|
|
159
|
-
score += Math.min(0.
|
|
193
|
+
// Numeric values (numbers with units are more specific)
|
|
194
|
+
const numbers = content.match(/\b\d+(?:\.\d+)?(?:-\w+)?\b/g) || [];
|
|
195
|
+
score += Math.min(0.35, numbers.length * 0.15);
|
|
160
196
|
|
|
161
197
|
// Code references (backticks, file paths)
|
|
162
198
|
const codeRefs = content.match(/`[^`]+`|[A-Za-z0-9_]+\.[A-Za-z0-9_]+/g) || [];
|
|
163
|
-
score += Math.min(0.
|
|
199
|
+
score += Math.min(0.4, codeRefs.length * 0.2);
|
|
164
200
|
|
|
165
201
|
// Technical terms (words with camelCase or snake_case)
|
|
166
202
|
const technicalTerms = content.match(/\b[a-z]+[A-Z][a-zA-Z]*\b|\b[a-z]+_[a-z_]+\b/g) || [];
|
|
167
|
-
score += Math.min(0.
|
|
203
|
+
score += Math.min(0.3, technicalTerms.length * 0.15);
|
|
204
|
+
|
|
205
|
+
// Long content is generally more specific
|
|
206
|
+
const wordCount = content.split(/\s+/).length;
|
|
207
|
+
if (wordCount > 10) score += 0.15;
|
|
208
|
+
if (wordCount > 18) score += 0.15;
|
|
168
209
|
|
|
169
210
|
return Math.min(1.0, score);
|
|
170
211
|
}
|
|
@@ -6,6 +6,10 @@ const policy = require("../policy");
|
|
|
6
6
|
const logger = require("../logger");
|
|
7
7
|
const { needsWebFallback } = require("../policy/web-fallback");
|
|
8
8
|
const promptCache = require("../cache/prompt");
|
|
9
|
+
const tokens = require("../utils/tokens");
|
|
10
|
+
const systemPrompt = require("../prompts/system");
|
|
11
|
+
const historyCompression = require("../context/compression");
|
|
12
|
+
const tokenBudget = require("../context/budget");
|
|
9
13
|
|
|
10
14
|
const DROP_KEYS = new Set([
|
|
11
15
|
"provider",
|
|
@@ -995,6 +999,45 @@ function sanitizePayload(payload) {
|
|
|
995
999
|
delete clean.tool_choice;
|
|
996
1000
|
}
|
|
997
1001
|
|
|
1002
|
+
// Smart tool selection (universal, applies to all providers)
|
|
1003
|
+
if (config.smartToolSelection?.enabled && Array.isArray(clean.tools) && clean.tools.length > 0) {
|
|
1004
|
+
const { classifyRequestType, selectToolsSmartly } = require('../tools/smart-selection');
|
|
1005
|
+
|
|
1006
|
+
const classification = classifyRequestType(clean);
|
|
1007
|
+
|
|
1008
|
+
logger.debug({
|
|
1009
|
+
originalToolCount: clean.tools.length,
|
|
1010
|
+
classificationType: classification.type,
|
|
1011
|
+
confidence: classification.confidence,
|
|
1012
|
+
keywords: classification.keywords
|
|
1013
|
+
}, "Smart tool selection: classified request");
|
|
1014
|
+
|
|
1015
|
+
const selectedTools = selectToolsSmartly(clean.tools, classification, {
|
|
1016
|
+
provider: providerType,
|
|
1017
|
+
tokenBudget: config.smartToolSelection.tokenBudget,
|
|
1018
|
+
config: config.smartToolSelection
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
const removedTools = clean.tools
|
|
1022
|
+
.filter(t => !selectedTools.find(s => s.name === t.name))
|
|
1023
|
+
.map(t => t.name);
|
|
1024
|
+
|
|
1025
|
+
const keptTools = selectedTools.map(t => t.name);
|
|
1026
|
+
|
|
1027
|
+
logger.info({
|
|
1028
|
+
feature: 'smart_tool_selection',
|
|
1029
|
+
requestType: classification.type,
|
|
1030
|
+
confidence: classification.confidence,
|
|
1031
|
+
originalToolCount: clean.tools.length,
|
|
1032
|
+
selectedToolCount: selectedTools.length,
|
|
1033
|
+
removedTools,
|
|
1034
|
+
keptTools,
|
|
1035
|
+
provider: providerType
|
|
1036
|
+
}, "Smart tool selection applied");
|
|
1037
|
+
|
|
1038
|
+
clean.tools = selectedTools.length > 0 ? selectedTools : undefined;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
998
1041
|
clean.stream = payload.stream ?? false;
|
|
999
1042
|
|
|
1000
1043
|
if (
|
|
@@ -1123,6 +1166,30 @@ async function runAgentLoop({
|
|
|
1123
1166
|
);
|
|
1124
1167
|
}
|
|
1125
1168
|
|
|
1169
|
+
|
|
1170
|
+
if (steps === 1 && config.historyCompression?.enabled !== false) {
|
|
1171
|
+
try {
|
|
1172
|
+
if (historyCompression.needsCompression(cleanPayload.messages)) {
|
|
1173
|
+
const originalMessages = cleanPayload.messages;
|
|
1174
|
+
cleanPayload.messages = historyCompression.compressHistory(originalMessages, {
|
|
1175
|
+
keepRecentTurns: config.historyCompression?.keepRecentTurns ?? 10,
|
|
1176
|
+
summarizeOlder: config.historyCompression?.summarizeOlder ?? true,
|
|
1177
|
+
enabled: true
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
if (cleanPayload.messages !== originalMessages) {
|
|
1181
|
+
const stats = historyCompression.calculateCompressionStats(originalMessages, cleanPayload.messages);
|
|
1182
|
+
logger.debug({
|
|
1183
|
+
sessionId: session?.id ?? null,
|
|
1184
|
+
...stats
|
|
1185
|
+
}, 'History compression applied');
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
} catch (err) {
|
|
1189
|
+
logger.warn({ err, sessionId: session?.id }, 'History compression failed, continuing with full history');
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1126
1193
|
// === MEMORY RETRIEVAL (Titans-inspired long-term memory) ===
|
|
1127
1194
|
if (config.memory?.enabled !== false && steps === 1) {
|
|
1128
1195
|
try {
|
|
@@ -1153,7 +1220,8 @@ async function runAgentLoop({
|
|
|
1153
1220
|
const injectedSystem = memoryRetriever.injectMemoriesIntoSystem(
|
|
1154
1221
|
cleanPayload.system,
|
|
1155
1222
|
relevantMemories,
|
|
1156
|
-
config.memory.injectionFormat ?? 'system'
|
|
1223
|
+
config.memory.injectionFormat ?? 'system',
|
|
1224
|
+
cleanPayload.messages // Pass recent messages for deduplication
|
|
1157
1225
|
);
|
|
1158
1226
|
|
|
1159
1227
|
if (typeof injectedSystem === 'string') {
|
|
@@ -1169,7 +1237,126 @@ async function runAgentLoop({
|
|
|
1169
1237
|
}
|
|
1170
1238
|
}
|
|
1171
1239
|
|
|
1172
|
-
|
|
1240
|
+
if (steps === 1 && (config.systemPrompt?.mode === 'dynamic' || config.systemPrompt?.toolDescriptions === 'minimal')) {
|
|
1241
|
+
try {
|
|
1242
|
+
// Compress tool descriptions if configured
|
|
1243
|
+
if (cleanPayload.tools && cleanPayload.tools.length > 0 && config.systemPrompt?.toolDescriptions === 'minimal') {
|
|
1244
|
+
const originalTools = cleanPayload.tools;
|
|
1245
|
+
cleanPayload.tools = systemPrompt.compressToolDescriptions(originalTools, 'minimal');
|
|
1246
|
+
|
|
1247
|
+
const originalSize = JSON.stringify(originalTools).length;
|
|
1248
|
+
const compressedSize = JSON.stringify(cleanPayload.tools).length;
|
|
1249
|
+
const saved = originalSize - compressedSize;
|
|
1250
|
+
|
|
1251
|
+
if (saved > 100) {
|
|
1252
|
+
logger.debug({
|
|
1253
|
+
sessionId: session?.id ?? null,
|
|
1254
|
+
toolCount: cleanPayload.tools.length,
|
|
1255
|
+
originalChars: originalSize,
|
|
1256
|
+
compressedChars: compressedSize,
|
|
1257
|
+
saved,
|
|
1258
|
+
percentage: ((saved / originalSize) * 100).toFixed(1)
|
|
1259
|
+
}, 'Tool descriptions compressed');
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// Optimize system prompt if configured
|
|
1264
|
+
if (cleanPayload.system && config.systemPrompt?.mode === 'dynamic') {
|
|
1265
|
+
const originalSystem = cleanPayload.system;
|
|
1266
|
+
const optimizedSystem = systemPrompt.optimizeSystemPrompt(
|
|
1267
|
+
originalSystem,
|
|
1268
|
+
{
|
|
1269
|
+
tools: cleanPayload.tools,
|
|
1270
|
+
messages: cleanPayload.messages
|
|
1271
|
+
},
|
|
1272
|
+
'dynamic'
|
|
1273
|
+
);
|
|
1274
|
+
|
|
1275
|
+
if (optimizedSystem !== originalSystem) {
|
|
1276
|
+
const savings = systemPrompt.calculateSavings(originalSystem, optimizedSystem);
|
|
1277
|
+
cleanPayload.system = optimizedSystem;
|
|
1278
|
+
|
|
1279
|
+
if (savings.tokensSaved > 50) {
|
|
1280
|
+
logger.debug({
|
|
1281
|
+
sessionId: session?.id ?? null,
|
|
1282
|
+
...savings
|
|
1283
|
+
}, 'System prompt optimized');
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
} catch (err) {
|
|
1288
|
+
logger.warn({ err, sessionId: session?.id }, 'System prompt optimization failed, continuing with original');
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
if (steps === 1 && config.tokenBudget?.enforcement !== false) {
|
|
1293
|
+
try {
|
|
1294
|
+
const budgetCheck = tokenBudget.checkBudget(cleanPayload);
|
|
1295
|
+
|
|
1296
|
+
if (budgetCheck.atWarning) {
|
|
1297
|
+
logger.warn({
|
|
1298
|
+
sessionId: session?.id ?? null,
|
|
1299
|
+
totalTokens: budgetCheck.totalTokens,
|
|
1300
|
+
warningThreshold: budgetCheck.warningThreshold,
|
|
1301
|
+
maxThreshold: budgetCheck.maxThreshold,
|
|
1302
|
+
overMax: budgetCheck.overMax
|
|
1303
|
+
}, 'Approaching or exceeding token budget');
|
|
1304
|
+
|
|
1305
|
+
if (budgetCheck.overMax) {
|
|
1306
|
+
// Apply adaptive compression to fit within budget
|
|
1307
|
+
const enforcement = tokenBudget.enforceBudget(cleanPayload, {
|
|
1308
|
+
warningThreshold: config.tokenBudget?.warning,
|
|
1309
|
+
maxThreshold: config.tokenBudget?.max,
|
|
1310
|
+
enforcement: true
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
if (enforcement.compressed) {
|
|
1314
|
+
cleanPayload = enforcement.payload;
|
|
1315
|
+
logger.info({
|
|
1316
|
+
sessionId: session?.id ?? null,
|
|
1317
|
+
strategy: enforcement.strategy,
|
|
1318
|
+
initialTokens: enforcement.stats.initialTokens,
|
|
1319
|
+
finalTokens: enforcement.stats.finalTokens,
|
|
1320
|
+
saved: enforcement.stats.saved,
|
|
1321
|
+
percentage: enforcement.stats.percentage,
|
|
1322
|
+
nowWithinBudget: !enforcement.finalBudget.overMax
|
|
1323
|
+
}, 'Token budget enforcement applied');
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
} catch (err) {
|
|
1328
|
+
logger.warn({ err, sessionId: session?.id }, 'Token budget enforcement failed, continuing without enforcement');
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// Track estimated token usage before model call
|
|
1333
|
+
const estimatedTokens = config.tokenTracking?.enabled !== false
|
|
1334
|
+
? tokens.countPayloadTokens(cleanPayload)
|
|
1335
|
+
: null;
|
|
1336
|
+
|
|
1337
|
+
if (estimatedTokens && config.tokenTracking?.enabled !== false) {
|
|
1338
|
+
logger.debug({
|
|
1339
|
+
sessionId: session?.id ?? null,
|
|
1340
|
+
estimated: estimatedTokens,
|
|
1341
|
+
model: cleanPayload.model
|
|
1342
|
+
}, 'Estimated token usage before model call');
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const databricksResponse = await invokeModel(cleanPayload);
|
|
1346
|
+
|
|
1347
|
+
// Extract and log actual token usage
|
|
1348
|
+
const actualUsage = databricksResponse.ok && config.tokenTracking?.enabled !== false
|
|
1349
|
+
? tokens.extractUsageFromResponse(databricksResponse.json)
|
|
1350
|
+
: null;
|
|
1351
|
+
|
|
1352
|
+
if (estimatedTokens && actualUsage && config.tokenTracking?.enabled !== false) {
|
|
1353
|
+
tokens.logTokenUsage('model_invocation', estimatedTokens, actualUsage);
|
|
1354
|
+
|
|
1355
|
+
// Record in session metadata
|
|
1356
|
+
if (session) {
|
|
1357
|
+
tokens.recordTokenUsage(session, steps, estimatedTokens, actualUsage, cleanPayload.model);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1173
1360
|
|
|
1174
1361
|
// Handle streaming responses (pass through without buffering)
|
|
1175
1362
|
if (databricksResponse.stream) {
|