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
|
@@ -7,14 +7,16 @@
|
|
|
7
7
|
* hard problems. An LLM reading the actual sentence knows better.
|
|
8
8
|
*
|
|
9
9
|
* DESIGN:
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* - Dedicated classifier model (CLASSIFIER_MODEL below), deliberately
|
|
11
|
+
* decoupled from tier serving — swapping TIER_* env vars does not change
|
|
12
|
+
* what classifies prompts. The tier DEPLOYMENT (which models the tiers
|
|
13
|
+
* route to) is surfaced to the classifier as tie-breaker context; see
|
|
14
|
+
* _buildTierContext.
|
|
13
15
|
* - Structured JSON output; parse failure → null → caller falls back.
|
|
14
|
-
* - Hard
|
|
16
|
+
* - Hard timeout (TIMEOUT_MS); on timeout → null → caller falls back.
|
|
15
17
|
* - LRU cache keyed by sha256(text.trim().toLowerCase()); capacity 500.
|
|
16
18
|
* - Skip conditions surface via classifyDifficulty returning null with
|
|
17
|
-
* reason=skipped: text
|
|
19
|
+
* reason=skipped: empty text, force-pattern matched, risk=high,
|
|
18
20
|
* cache hit is transparent (returns cached).
|
|
19
21
|
* - Hardcoded kill-switch CLASSIFIER_ENABLED — no env var per user policy.
|
|
20
22
|
*
|
|
@@ -31,7 +33,6 @@ const CLASSIFIER_ENABLED = true;
|
|
|
31
33
|
// LRU cache to keep amortized latency low.
|
|
32
34
|
const TIMEOUT_MS = 10000;
|
|
33
35
|
const CACHE_CAPACITY = 500;
|
|
34
|
-
const MIN_TEXT_LENGTH = 15;
|
|
35
36
|
|
|
36
37
|
// Classifier model — decoupled from tier serving so SIMPLE tier can run a
|
|
37
38
|
// more capable model for real traffic while the classifier stays fast and
|
|
@@ -44,23 +45,145 @@ const CLASSIFIER_MODEL = 'qwen2.5:3b';
|
|
|
44
45
|
|
|
45
46
|
const VALID_TIERS = ['SIMPLE', 'MEDIUM', 'COMPLEX', 'REASONING'];
|
|
46
47
|
|
|
47
|
-
// One-shot classification prompt. Kept in a const so drift is diffable.
|
|
48
|
+
// One-shot classification prompt (v3). Kept in a const so drift is diffable.
|
|
48
49
|
// Difficulty framing (not intent) — matches config B routing goals.
|
|
49
|
-
|
|
50
|
+
//
|
|
51
|
+
// v2 (2026-07-21): added the follow-up rule, negative examples under
|
|
52
|
+
// REASONING, and casual-question SIMPLE examples after a live incident:
|
|
53
|
+
// qwen2.5:3b read "Who kills him ?" (a Doctor Doom plot question) as
|
|
54
|
+
// REASONING conf 1.0 — v1's REASONING examples were all formal-methods
|
|
55
|
+
// flavored and nothing said surface vocabulary isn't the signal. Baseline
|
|
56
|
+
// on data/difficulty-eval-followups.jsonl: 60% overall, 33% on SIMPLE,
|
|
57
|
+
// 3 SIMPLE→REASONING criticals.
|
|
58
|
+
//
|
|
59
|
+
// v3 (2026-08-09): added trivial-arithmetic SIMPLE examples after a live
|
|
60
|
+
// over-route: "12+21" scored anchor 25 (MEDIUM band) and qwen ALSO said
|
|
61
|
+
// MEDIUM conf 1.0 — bare arithmetic read as "a specific mechanical task".
|
|
62
|
+
// Any tiny model adds two numbers; it belongs in SIMPLE.
|
|
63
|
+
const CLASSIFY_PROMPT = `You are a classifier for an LLM routing proxy. Classify the difficulty of the CURRENT user prompt into exactly one of four tiers. Reply with ONLY valid JSON on a single line, no other text.
|
|
50
64
|
|
|
51
65
|
Tiers:
|
|
52
|
-
- SIMPLE: casual acknowledgments, greetings, one-word answers, trivial factual lookups. Any tiny model handles.
|
|
53
|
-
examples: "hi", "ok thanks", "yes continue", "what time is it"
|
|
66
|
+
- SIMPLE: casual acknowledgments, greetings, one-word answers, trivial factual lookups, and short conversational follow-up questions about people, stories, events, or everyday facts. Any tiny model handles.
|
|
67
|
+
examples: "hi", "ok thanks", "yes continue", "what time is it", "who is doctor doom?", "who kills him?", "why did he do that?", "and then what happened?", "does bleach kill mold?", "12+21", "what is 15% of 80?", "convert 3km to miles"
|
|
54
68
|
- MEDIUM: one specific mechanical task or a focused explanation. Mid-size local model suffices.
|
|
55
|
-
examples: "list the exports from this file", "run the unit tests", "fix the linter warnings", "explain this regex", "add error handling to this block"
|
|
69
|
+
examples: "list the exports from this file", "run the unit tests", "fix the linter warnings", "explain this regex", "add error handling to this block", "verify the file exists before reading it"
|
|
56
70
|
- COMPLEX: multi-file design, systemic refactor, architecture review, debugging that requires broad code understanding. Needs a strong general model.
|
|
57
71
|
examples: "architecture review of the orchestrator", "refactor the entire ingestion pipeline", "debug this complex race condition across three services"
|
|
58
72
|
- REASONING: formal proof, correctness verification, security audit, novel algorithm design, formal reasoning from first principles. Needs a frontier reasoning model.
|
|
59
73
|
examples: "prove the correctness of this lock-free queue", "security audit the auth middleware", "formally verify this state machine never deadlocks", "derive the optimal eviction policy and prove its competitive ratio"
|
|
74
|
+
NOT reasoning: casual questions that merely contain words like "prove", "kill", "verify", "audit" — "who kills him?" is SIMPLE, "prove me wrong lol" is SIMPLE, "can you verify the score?" is SIMPLE, "verify the file exists" is MEDIUM.
|
|
75
|
+
|
|
76
|
+
Rules:
|
|
77
|
+
- Judge the TASK the model must perform, not the vocabulary. Words like kill/prove/verify/audit/security do not make a prompt REASONING unless it demands formal or expert-level analysis.
|
|
78
|
+
- A short follow-up question (pronouns like he/she/it/that referring to the earlier conversation) about a casual topic is SIMPLE. A follow-up that extends a technical task inherits the difficulty of that task.
|
|
60
79
|
|
|
61
80
|
Reply format (strict): {"tier":"SIMPLE|MEDIUM|COMPLEX|REASONING","confidence":0.0-1.0}
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
// Short prompts are where context matters: "Who kills him ?" is
|
|
84
|
+
// unclassifiable in isolation but trivially SIMPLE next to its conversation.
|
|
85
|
+
// Long prompts self-describe, and contextualizing them would only shrink the
|
|
86
|
+
// LRU hit rate and grow latency.
|
|
87
|
+
const CONTEXT_MAX_TEXT_LENGTH = 40;
|
|
88
|
+
const CONTEXT_MAX_CHARS = 300;
|
|
89
|
+
|
|
90
|
+
// --- Tier deployment context -------------------------------------------------
|
|
91
|
+
//
|
|
92
|
+
// Tells the classifier WHAT each tier routes to (model + coarse traits from
|
|
93
|
+
// the registry), so borderline verdicts can weigh the actual fleet — e.g.
|
|
94
|
+
// prefer MEDIUM when its configured model is a large cloud model, or lean
|
|
95
|
+
// away from a tier whose model lacks the needed capability.
|
|
96
|
+
//
|
|
97
|
+
// Constraints that shape the design:
|
|
98
|
+
// - TIE-BREAKER ONLY. The difficulty rubric in CLASSIFY_PROMPT governs;
|
|
99
|
+
// the deployment block must not redefine what the tiers mean, or eval
|
|
100
|
+
// labels (data/difficulty-eval*.jsonl) stop being comparable.
|
|
101
|
+
// - BYTE-STABLE per process. Tier config is env-driven and fixed for the
|
|
102
|
+
// process lifetime, so the block is built once and memoized. It also
|
|
103
|
+
// participates in the LRU cache key (fingerprint) so a reconfigured
|
|
104
|
+
// deployment never serves verdicts cached under the old one.
|
|
105
|
+
// - FAIL-SAFE. Registry misses or config errors degrade to provider-type
|
|
106
|
+
// traits or to no block at all — classification proceeds regardless.
|
|
107
|
+
// MEASURED OFF (2026-08-08): A/B on data/difficulty-eval-followups.jsonl —
|
|
108
|
+
// baseline 84.0% overall / 0 SIMPLE→REASONING criticals; with the block
|
|
109
|
+
// 76.0% / 1 critical ('prove me wrong lol' → REASONING conf 1.0, the exact
|
|
110
|
+
// failure class prompt v2 was built to kill). qwen2.5:3b can't use fleet
|
|
111
|
+
// info — it dilutes the rubric. Machinery kept dark for a future stronger
|
|
112
|
+
// classifier: flip this flag, then re-run
|
|
113
|
+
// scripts/validate-difficulty-classifier.js on BOTH eval files and hold
|
|
114
|
+
// the ship bar (>=85% overall, zero MEDIUM→REASONING false positives).
|
|
115
|
+
const TIER_CONTEXT_ENABLED = false;
|
|
116
|
+
const LOCAL_PROVIDERS = new Set(['ollama', 'llamacpp', 'lmstudio']);
|
|
117
|
+
|
|
118
|
+
/** Pure builder — exported for tests. @param {Object} tiers {TIER: 'provider:model'} */
|
|
119
|
+
function _buildTierContext(tiers) {
|
|
120
|
+
const lines = [];
|
|
121
|
+
for (const tier of VALID_TIERS) {
|
|
122
|
+
const spec = tiers?.[tier];
|
|
123
|
+
if (typeof spec !== 'string' || !spec.includes(':')) continue;
|
|
124
|
+
const sep = spec.indexOf(':');
|
|
125
|
+
const provider = spec.slice(0, sep);
|
|
126
|
+
const model = spec.slice(sep + 1);
|
|
127
|
+
const traits = [LOCAL_PROVIDERS.has(provider) ? 'local' : 'cloud'];
|
|
128
|
+
try {
|
|
129
|
+
const { getModelRegistrySync } = require('./model-registry');
|
|
130
|
+
const cost = getModelRegistrySync().getCost(model);
|
|
131
|
+
if (cost && !cost.unknown) {
|
|
132
|
+
if (Number.isFinite(cost.context) && cost.context >= 1000) {
|
|
133
|
+
traits.push(`${Math.round(cost.context / 1000)}k ctx`);
|
|
134
|
+
}
|
|
135
|
+
if (cost.reasoning) traits.push('reasoning');
|
|
136
|
+
if (cost.vision) traits.push('vision');
|
|
137
|
+
}
|
|
138
|
+
} catch { /* registry unavailable — provider trait only */ }
|
|
139
|
+
lines.push(`- ${tier} routes to ${model} (${traits.join(', ')})`);
|
|
140
|
+
}
|
|
141
|
+
if (!lines.length) return null;
|
|
142
|
+
const block = `Deployment (what each tier currently routes to — use only as a tie-breaker between adjacent tiers; the tier definitions above still govern):
|
|
143
|
+
${lines.join('\n')}`;
|
|
144
|
+
return {
|
|
145
|
+
block,
|
|
146
|
+
fingerprint: crypto.createHash('sha256').update(block).digest('hex').slice(0, 16),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** @type {{block:string, fingerprint:string}|null|undefined} undefined = not built yet */
|
|
151
|
+
let _tierContext;
|
|
152
|
+
|
|
153
|
+
function _tierDeploymentContext() {
|
|
154
|
+
// Test override (set via _setTierContextForTests) wins over the flag so
|
|
155
|
+
// the machinery stays exercised while shipping dark.
|
|
156
|
+
if (_tierContext !== undefined) return _tierContext;
|
|
157
|
+
if (!TIER_CONTEXT_ENABLED) { _tierContext = null; return null; }
|
|
158
|
+
try {
|
|
159
|
+
const config = require('../config');
|
|
160
|
+
_tierContext = _buildTierContext(config.modelTiers);
|
|
161
|
+
} catch {
|
|
162
|
+
_tierContext = null;
|
|
163
|
+
}
|
|
164
|
+
return _tierContext;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Test helper — override or reset (pass undefined) the memoized block. */
|
|
168
|
+
function _setTierContextForTests(tiers) {
|
|
169
|
+
_tierContext = tiers === undefined ? undefined : _buildTierContext(tiers);
|
|
170
|
+
}
|
|
62
171
|
|
|
63
|
-
|
|
172
|
+
function _buildPrompt(text, context) {
|
|
173
|
+
const tierCtx = _tierDeploymentContext();
|
|
174
|
+
const preamble = tierCtx ? `${CLASSIFY_PROMPT}
|
|
175
|
+
${tierCtx.block}
|
|
176
|
+
` : CLASSIFY_PROMPT;
|
|
177
|
+
if (context) {
|
|
178
|
+
return `${preamble}
|
|
179
|
+
Conversation so far (context only — classify the CURRENT prompt, inheriting topic difficulty per the rules):
|
|
180
|
+
${context}
|
|
181
|
+
|
|
182
|
+
CURRENT user prompt: """${text}"""`;
|
|
183
|
+
}
|
|
184
|
+
return `${preamble}
|
|
185
|
+
User prompt: """${text}"""`;
|
|
186
|
+
}
|
|
64
187
|
|
|
65
188
|
// --- LRU cache --------------------------------------------------------------
|
|
66
189
|
|
|
@@ -95,7 +218,7 @@ function _cacheKey(text) {
|
|
|
95
218
|
|
|
96
219
|
// --- Ollama dispatch (only supported provider for the SIMPLE tier today) ----
|
|
97
220
|
|
|
98
|
-
async function _callOllama(text, opts) {
|
|
221
|
+
async function _callOllama(text, context, opts) {
|
|
99
222
|
const config = require('../config');
|
|
100
223
|
|
|
101
224
|
// First cut supports ollama-family classifier only. Other providers can
|
|
@@ -109,7 +232,7 @@ async function _callOllama(text, opts) {
|
|
|
109
232
|
const url = `${endpoint.replace(/\/$/, '')}/api/chat`;
|
|
110
233
|
const body = {
|
|
111
234
|
model: CLASSIFIER_MODEL,
|
|
112
|
-
messages: [{ role: 'user', content:
|
|
235
|
+
messages: [{ role: 'user', content: _buildPrompt(text, context) }],
|
|
113
236
|
stream: false,
|
|
114
237
|
format: 'json',
|
|
115
238
|
// num_predict generous because thinking models (minimax-m2.5) burn
|
|
@@ -171,6 +294,9 @@ function _parseResult(raw) {
|
|
|
171
294
|
* @param {number} [opts.timeoutMs] — override default 2500ms
|
|
172
295
|
* @param {boolean} [opts.forceMatched] — caller already matched a FORCE_* pattern; skip classifier
|
|
173
296
|
* @param {string} [opts.riskLevel] — 'high' | 'medium' | 'low'; skip when 'high'
|
|
297
|
+
* @param {string} [opts.context] — condensed prior conversation ("user asked:
|
|
298
|
+
* ... → assistant replied about: ..."); used only for short prompts, where
|
|
299
|
+
* a bare follow-up is unclassifiable in isolation
|
|
174
300
|
* @returns {Promise<{tier:string,confidence:number,source:'cache'|'model'}|null>}
|
|
175
301
|
* null when: disabled, skipped, model failure, parse failure, timeout.
|
|
176
302
|
*/
|
|
@@ -178,15 +304,30 @@ async function classifyDifficulty(text, opts = {}) {
|
|
|
178
304
|
if (!CLASSIFIER_ENABLED) return null;
|
|
179
305
|
if (typeof text !== 'string') return null;
|
|
180
306
|
const trimmed = text.trim();
|
|
181
|
-
|
|
307
|
+
// No minimum length (removed 2026-08-09): short prompts are exactly what
|
|
308
|
+
// the v2 context feature was built for, and the 15-char gate meant the
|
|
309
|
+
// classifier could never rescue anchor over-reads like '12+21' -> MEDIUM.
|
|
310
|
+
// Greetings still skip upstream via force patterns (opts.forceMatched),
|
|
311
|
+
// and the LRU absorbs repeats, so the added model calls are bounded.
|
|
312
|
+
if (!trimmed) return null;
|
|
182
313
|
if (opts.forceMatched) return null;
|
|
183
314
|
if (opts.riskLevel === 'high') return null;
|
|
184
315
|
|
|
185
|
-
const
|
|
316
|
+
const context =
|
|
317
|
+
typeof opts.context === 'string' && opts.context.trim() && trimmed.length <= CONTEXT_MAX_TEXT_LENGTH
|
|
318
|
+
? opts.context.trim().slice(0, CONTEXT_MAX_CHARS)
|
|
319
|
+
: null;
|
|
320
|
+
|
|
321
|
+
// Context participates in the cache key: the same follow-up text means
|
|
322
|
+
// different things in different conversations. The tier-deployment
|
|
323
|
+
// fingerprint participates too (NUL separators are collision-proof), so
|
|
324
|
+
// verdicts cached under one deployment never serve a reconfigured one.
|
|
325
|
+
const tierFp = _tierDeploymentContext()?.fingerprint ?? "";
|
|
326
|
+
const key = _cacheKey(`${tierFp}${context ? `${trimmed}${context}` : trimmed}`);
|
|
186
327
|
const cached = _cache.get(key);
|
|
187
328
|
if (cached) return { ...cached, source: 'cache' };
|
|
188
329
|
|
|
189
|
-
const raw = await _callOllama(trimmed, opts);
|
|
330
|
+
const raw = await _callOllama(trimmed, context, opts);
|
|
190
331
|
const parsed = _parseResult(raw);
|
|
191
332
|
if (!parsed) return null;
|
|
192
333
|
_cache.set(key, parsed);
|
|
@@ -214,6 +355,9 @@ module.exports = {
|
|
|
214
355
|
// internals for tests only
|
|
215
356
|
_parseResult,
|
|
216
357
|
_cacheKey,
|
|
358
|
+
_buildPrompt,
|
|
359
|
+
_buildTierContext,
|
|
360
|
+
_setTierContextForTests,
|
|
217
361
|
_clearCacheForTests,
|
|
218
362
|
_getCacheStats,
|
|
219
363
|
};
|