lynkr 9.9.1 → 9.11.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/README.md +50 -9
- package/bin/cli.js +2 -0
- package/bin/lynkr-init.js +11 -12
- package/bin/lynkr-reset.js +71 -0
- package/config/model-tiers.json +199 -52
- package/package.json +3 -3
- package/public/dashboard.html +29 -0
- package/scripts/validate-difficulty-classifier.js +27 -6
- package/src/api/providers-handler.js +0 -1
- package/src/api/router.js +309 -160
- package/src/clients/databricks.js +215 -71
- package/src/clients/gpt-utils.js +8 -1
- package/src/clients/ollama-utils.js +19 -10
- package/src/clients/prompt-cache-injection.js +93 -30
- package/src/config/index.js +82 -43
- package/src/context/compression.js +27 -8
- package/src/context/distill.js +2 -1
- package/src/dashboard/api.js +5 -1
- package/src/memory/distiller.js +350 -0
- package/src/memory/extractor.js +0 -8
- package/src/memory/format.js +17 -2
- package/src/memory/index.js +10 -0
- package/src/memory/retriever.js +7 -2
- package/src/memory/skills-cache.js +180 -0
- package/src/memory/tencentdb-launcher.js +459 -0
- package/src/memory/wiki.js +171 -0
- package/src/orchestrator/index.js +202 -1237
- package/src/orchestrator/passthrough-stream.js +382 -0
- package/src/orchestrator/sse-transformer.js +408 -0
- package/src/prompts/system.js +20 -6
- package/src/routing/affinity-store.js +85 -4
- package/src/routing/agentic-detector.js +9 -1
- package/src/routing/cache-economics.js +123 -0
- package/src/routing/cache-switch-cost.js +194 -0
- package/src/routing/calibration.js +8 -1
- package/src/routing/complexity-analyzer.js +7 -1
- package/src/routing/context-compressor.js +170 -0
- package/src/routing/difficulty-classifier.js +161 -17
- package/src/routing/index.js +196 -56
- package/src/routing/intent-score.js +20 -2
- package/src/routing/model-registry.js +35 -10
- package/src/routing/session-affinity.js +68 -1
- package/src/routing/side-channel-detector.js +103 -0
- package/src/routing/telemetry.js +139 -2
- package/src/routing/verifier.js +41 -2
- package/src/server.js +36 -49
- package/src/tools/code-graph.js +6 -4
- package/src/agents/context-manager.js +0 -236
- package/src/agents/decomposition/dispatcher.js +0 -185
- package/src/agents/decomposition/gate.js +0 -136
- package/src/agents/decomposition/index.js +0 -183
- package/src/agents/decomposition/model-call.js +0 -75
- package/src/agents/decomposition/planner.js +0 -223
- package/src/agents/decomposition/synthesizer.js +0 -89
- package/src/agents/decomposition/telemetry.js +0 -55
- package/src/agents/definitions/loader.js +0 -653
- package/src/agents/executor.js +0 -457
- package/src/agents/index.js +0 -165
- package/src/agents/parallel-coordinator.js +0 -68
- package/src/agents/reflector.js +0 -331
- package/src/agents/skillbook.js +0 -331
- package/src/agents/store.js +0 -259
- package/src/edits/index.js +0 -171
- package/src/indexer/babel-parser.js +0 -213
- package/src/indexer/index.js +0 -1629
- package/src/indexer/navigation/index.js +0 -32
- package/src/indexer/navigation/providers/treeSitter.js +0 -36
- package/src/indexer/parser.js +0 -443
- package/src/tasks/store.js +0 -349
- package/src/tests/coverage.js +0 -173
- package/src/tests/index.js +0 -171
- package/src/tests/store.js +0 -213
- package/src/tools/agent-task.js +0 -145
- package/src/tools/code-mode.js +0 -304
- package/src/tools/decompose.js +0 -91
- package/src/tools/edits.js +0 -94
- package/src/tools/execution.js +0 -171
- package/src/tools/git.js +0 -1346
- package/src/tools/index.js +0 -306
- package/src/tools/indexer.js +0 -360
- package/src/tools/lazy-loader.js +0 -366
- package/src/tools/mcp-remote.js +0 -88
- package/src/tools/mcp.js +0 -116
- package/src/tools/process.js +0 -167
- package/src/tools/smart-selection.js +0 -180
- package/src/tools/stubs.js +0 -55
- package/src/tools/tasks.js +0 -260
- package/src/tools/tests.js +0 -132
- package/src/tools/tinyfish.js +0 -358
- package/src/tools/truncate.js +0 -106
- package/src/tools/web-client.js +0 -71
- package/src/tools/web.js +0 -415
- package/src/tools/workspace.js +0 -204
package/src/memory/format.js
CHANGED
|
@@ -20,11 +20,26 @@ function formatMemoriesForContext(memories, format = 'compact') {
|
|
|
20
20
|
return formatCompact(memories);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Deterministic render order (cache-aware routing, Phase 5): retrieval
|
|
25
|
+
* returns equal-relevance memories in unstable order, and the rendered
|
|
26
|
+
* bullets sit in the system prompt — the very front of the provider's
|
|
27
|
+
* prompt-cache prefix. A reordered bullet list is a byte change that
|
|
28
|
+
* silently invalidates the entire cached prefix (observed live: identical
|
|
29
|
+
* memory sets flipping order between consecutive requests). Sorting by
|
|
30
|
+
* content pins the bytes; presentation order carries no meaning here.
|
|
31
|
+
*/
|
|
32
|
+
function stableOrder(memories) {
|
|
33
|
+
return [...memories].sort((a, b) =>
|
|
34
|
+
String(a.content) < String(b.content) ? -1 : String(a.content) > String(b.content) ? 1 : 0
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
/**
|
|
24
39
|
* Compact memory format - 75% fewer tokens
|
|
25
40
|
*/
|
|
26
41
|
function formatCompact(memories) {
|
|
27
|
-
const items = memories
|
|
42
|
+
const items = stableOrder(memories)
|
|
28
43
|
.map(mem => `- ${mem.content}`)
|
|
29
44
|
.join('\n');
|
|
30
45
|
|
|
@@ -35,7 +50,7 @@ function formatCompact(memories) {
|
|
|
35
50
|
* Verbose XML format (original)
|
|
36
51
|
*/
|
|
37
52
|
function formatVerbose(memories) {
|
|
38
|
-
const items = memories.map((mem, idx) => {
|
|
53
|
+
const items = stableOrder(memories).map((mem, idx) => {
|
|
39
54
|
const age = formatAge(mem.createdAt);
|
|
40
55
|
const type = mem.type ? `[${mem.type}] ` : '';
|
|
41
56
|
return `${idx + 1}. ${type}${mem.content} (${age})`;
|
package/src/memory/index.js
CHANGED
|
@@ -15,6 +15,9 @@ const retriever = require("./retriever");
|
|
|
15
15
|
const extractor = require("./extractor");
|
|
16
16
|
const surprise = require("./surprise");
|
|
17
17
|
const tools = require("./tools");
|
|
18
|
+
const distiller = require("./distiller");
|
|
19
|
+
const wiki = require("./wiki");
|
|
20
|
+
const skillsCache = require("./skills-cache");
|
|
18
21
|
|
|
19
22
|
module.exports = {
|
|
20
23
|
// Store operations
|
|
@@ -52,4 +55,11 @@ module.exports = {
|
|
|
52
55
|
// Tools
|
|
53
56
|
tools,
|
|
54
57
|
MEMORY_TOOLS: tools.MEMORY_TOOLS,
|
|
58
|
+
|
|
59
|
+
// TencentDB-inspired L0-L3 token optimization
|
|
60
|
+
distiller,
|
|
61
|
+
needsDistillation: distiller.needsDistillation,
|
|
62
|
+
distillMessages: distiller.distillMessages,
|
|
63
|
+
wiki,
|
|
64
|
+
skillsCache,
|
|
55
65
|
};
|
package/src/memory/retriever.js
CHANGED
|
@@ -3,6 +3,10 @@ const search = require("./search");
|
|
|
3
3
|
const logger = require("../logger");
|
|
4
4
|
const format = require("./format");
|
|
5
5
|
|
|
6
|
+
// Infrastructure memory types (wiki registry, compression skills) are used
|
|
7
|
+
// by the distillation pipeline, never injected as conversational context
|
|
8
|
+
const INTERNAL_MEMORY_TYPES = new Set(["wiki", "skill"]);
|
|
9
|
+
|
|
6
10
|
/**
|
|
7
11
|
* Retrieve relevant memories using multi-signal ranking
|
|
8
12
|
*
|
|
@@ -41,8 +45,9 @@ function retrieveRelevantMemories(query, options = {}) {
|
|
|
41
45
|
sessionId: includeGlobal ? null : sessionId,
|
|
42
46
|
});
|
|
43
47
|
|
|
44
|
-
// 4. Merge and deduplicate
|
|
45
|
-
const candidates = mergeUnique([ftsResults, recentMemories, importantMemories])
|
|
48
|
+
// 4. Merge and deduplicate, dropping infrastructure entries
|
|
49
|
+
const candidates = mergeUnique([ftsResults, recentMemories, importantMemories])
|
|
50
|
+
.filter(m => !INTERNAL_MEMORY_TYPES.has(m.type));
|
|
46
51
|
|
|
47
52
|
// 5. Score and rank
|
|
48
53
|
const scored = candidates.map(memory => ({
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Cache — Compression Strategy Memory (TencentDB-Agent-Memory inspired)
|
|
3
|
+
*
|
|
4
|
+
* Remembers which compression method worked (and how well) for each
|
|
5
|
+
* structural shape of tool output. Validated skills (savings >= minSavings)
|
|
6
|
+
* persist in the memories table (type='skill') and let future compressions:
|
|
7
|
+
* - skip section-dedup for shapes where it never helps (latency win)
|
|
8
|
+
* - compress proven-compressible shapes more aggressively (token win)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const store = require("./store");
|
|
12
|
+
const distill = require("../context/distill");
|
|
13
|
+
const config = require("../config");
|
|
14
|
+
const logger = require("../logger");
|
|
15
|
+
|
|
16
|
+
const MAX_CACHED_SKILLS = 1000;
|
|
17
|
+
const SIGNATURE_LINES = 30;
|
|
18
|
+
|
|
19
|
+
// signature -> { method, avgSavings, hits, persistedId }
|
|
20
|
+
let skillMap = null;
|
|
21
|
+
|
|
22
|
+
function skillsConfig() {
|
|
23
|
+
return config.memory?.skills ?? {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Structural shape signature for a tool output: leading token of each of
|
|
28
|
+
* the first N normalized lines. Two grep outputs, two test runs, or two
|
|
29
|
+
* JSON blobs with the same shape produce the same signature even when the
|
|
30
|
+
* values differ.
|
|
31
|
+
*/
|
|
32
|
+
function shapeSignature(text) {
|
|
33
|
+
if (!text) return "empty";
|
|
34
|
+
const lines = distill.normalizeText(text).split("\n").slice(0, SIGNATURE_LINES);
|
|
35
|
+
const shape = lines
|
|
36
|
+
.map(l => {
|
|
37
|
+
const trimmed = l.trim();
|
|
38
|
+
if (!trimmed) return "";
|
|
39
|
+
// Leading structural token: punctuation kept, words folded
|
|
40
|
+
const lead = trimmed.match(/^[{}[\]"'\-+*#>|]|^[A-Za-z_$]+|^\d+/);
|
|
41
|
+
return lead ? (/^\d+$/.test(lead[0]) ? "N" : lead[0]) : trimmed[0];
|
|
42
|
+
})
|
|
43
|
+
.join(",");
|
|
44
|
+
// djb2 hash keeps keys short
|
|
45
|
+
let hash = 5381;
|
|
46
|
+
for (let i = 0; i < shape.length; i++) {
|
|
47
|
+
hash = ((hash << 5) + hash + shape.charCodeAt(i)) | 0;
|
|
48
|
+
}
|
|
49
|
+
return `s${lines.length}_${(hash >>> 0).toString(36)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function loadCache() {
|
|
53
|
+
if (skillMap) return skillMap;
|
|
54
|
+
|
|
55
|
+
skillMap = new Map();
|
|
56
|
+
try {
|
|
57
|
+
const rows = store.getMemoriesByType("skill", MAX_CACHED_SKILLS);
|
|
58
|
+
for (const row of rows) {
|
|
59
|
+
const meta = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
|
|
60
|
+
if (!meta?.signature) continue;
|
|
61
|
+
skillMap.set(meta.signature, {
|
|
62
|
+
method: meta.method,
|
|
63
|
+
avgSavings: meta.avgSavings ?? 0,
|
|
64
|
+
hits: meta.hits ?? 1,
|
|
65
|
+
persistedId: row.id,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
logger.warn({ err }, "[skills] Failed to load skills, starting empty");
|
|
70
|
+
}
|
|
71
|
+
return skillMap;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Look up a known compression skill for a shape signature.
|
|
76
|
+
* @returns {{method: string, avgSavings: number, hits: number}|null}
|
|
77
|
+
*/
|
|
78
|
+
function lookup(signature) {
|
|
79
|
+
if (skillsConfig().enabled === false) return null;
|
|
80
|
+
return loadCache().get(signature) ?? null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Record a compression outcome. Kept in memory always (so known-futile
|
|
85
|
+
* shapes can skip dedup work); persisted only when the running average
|
|
86
|
+
* savings clears minSavings — the "validated skill" bar.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} signature - shapeSignature() of the original text
|
|
89
|
+
* @param {string} method - compression method that ran ('delta'|'distill'|'passthrough')
|
|
90
|
+
* @param {number} savings - fraction saved, 0..1
|
|
91
|
+
*/
|
|
92
|
+
function record(signature, method, savings) {
|
|
93
|
+
if (skillsConfig().enabled === false || !signature) return;
|
|
94
|
+
|
|
95
|
+
const cache = loadCache();
|
|
96
|
+
const existing = cache.get(signature);
|
|
97
|
+
const entry = existing
|
|
98
|
+
? {
|
|
99
|
+
...existing,
|
|
100
|
+
method,
|
|
101
|
+
hits: existing.hits + 1,
|
|
102
|
+
avgSavings: (existing.avgSavings * existing.hits + savings) / (existing.hits + 1),
|
|
103
|
+
}
|
|
104
|
+
: { method, avgSavings: savings, hits: 1, persistedId: null };
|
|
105
|
+
|
|
106
|
+
cache.set(signature, entry);
|
|
107
|
+
if (cache.size > MAX_CACHED_SKILLS) {
|
|
108
|
+
cache.delete(cache.keys().next().value);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const minSavings = skillsConfig().minSavings ?? 0.6;
|
|
112
|
+
if (entry.avgSavings < minSavings) return;
|
|
113
|
+
|
|
114
|
+
// Persist validated skill off the hot path
|
|
115
|
+
setImmediate(() => {
|
|
116
|
+
try {
|
|
117
|
+
const metadata = {
|
|
118
|
+
signature,
|
|
119
|
+
method: entry.method,
|
|
120
|
+
avgSavings: entry.avgSavings,
|
|
121
|
+
hits: entry.hits,
|
|
122
|
+
};
|
|
123
|
+
if (entry.persistedId) {
|
|
124
|
+
const current = store.getMemory(entry.persistedId);
|
|
125
|
+
if (current) {
|
|
126
|
+
store.updateMemory(entry.persistedId, { metadata });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const memory = store.createMemory({
|
|
131
|
+
sessionId: null,
|
|
132
|
+
content: `Compression skill: ${entry.method} saves ${(entry.avgSavings * 100).toFixed(0)}% on shape ${signature}`,
|
|
133
|
+
type: "skill",
|
|
134
|
+
category: "optimization",
|
|
135
|
+
importance: 0.3,
|
|
136
|
+
metadata,
|
|
137
|
+
});
|
|
138
|
+
entry.persistedId = memory.id;
|
|
139
|
+
} catch (err) {
|
|
140
|
+
logger.warn({ err, signature }, "[skills] Failed to persist skill");
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Compression hints for a given text, derived from validated skills.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} text - Tool result text about to be compressed
|
|
149
|
+
* @returns {{signature: string, skipDedup: boolean, maxLengthFactor: number}}
|
|
150
|
+
*/
|
|
151
|
+
function getCompressionHints(text) {
|
|
152
|
+
const signature = shapeSignature(text);
|
|
153
|
+
const skill = lookup(signature);
|
|
154
|
+
const minSavings = skillsConfig().minSavings ?? 0.6;
|
|
155
|
+
|
|
156
|
+
if (!skill || skill.hits < 2) {
|
|
157
|
+
return { signature, skipDedup: false, maxLengthFactor: 1 };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
signature,
|
|
162
|
+
// Dedup/delta repeatedly achieved almost nothing — don't burn CPU on it
|
|
163
|
+
skipDedup: skill.method === "passthrough" || skill.avgSavings < 0.05,
|
|
164
|
+
// Proven highly-compressible shape — compress harder
|
|
165
|
+
maxLengthFactor: skill.avgSavings >= minSavings ? 0.8 : 1,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Reset the in-memory cache (test support). */
|
|
170
|
+
function resetCache() {
|
|
171
|
+
skillMap = null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
shapeSignature,
|
|
176
|
+
lookup,
|
|
177
|
+
record,
|
|
178
|
+
getCompressionHints,
|
|
179
|
+
resetCache,
|
|
180
|
+
};
|