lynkr 9.14.16 → 9.14.18
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 +2 -0
- package/bin/cli.js +2 -0
- package/bin/lynkr-audit.js +58 -0
- package/bin/lynkr-route.js +76 -0
- package/config/complexity-exemplars.json +366 -0
- package/config/routing.example.json +186 -0
- package/config/routing.json +93 -0
- package/package.json +2 -2
- package/scripts/build-complexity-exemplars.js +84 -0
- package/scripts/build-routing-corpus.js +55 -0
- package/scripts/cache-probe.js +66 -0
- package/scripts/calibrate-capabilities.js +433 -0
- package/scripts/eval-complexity-signal.js +65 -0
- package/scripts/validate-grounding.js +63 -0
- package/src/api/router.js +53 -4
- package/src/cache/embeddings.js +22 -3
- package/src/clients/cursor-utils.js +1 -1
- package/src/clients/databricks.js +258 -124
- package/src/context/output-format-guard.js +1 -0
- package/src/orchestrator/index.js +11 -2
- package/src/routing/agentic-detector.js +6 -1
- package/src/routing/client-profiles.js +22 -0
- package/src/routing/complexity-analyzer.js +7 -0
- package/src/routing/decisions.js +100 -0
- package/src/routing/grounding.js +118 -0
- package/src/routing/harness-envelope.js +71 -1
- package/src/routing/index.js +104 -7
- package/src/routing/intent-score.js +5 -0
- package/src/routing/outcomes.js +136 -0
- package/src/routing/routing-config.js +154 -0
- package/src/routing/shortfall.js +90 -1
- package/src/routing/signals.js +179 -0
- package/src/routing/switch-gate.js +90 -0
- package/src/routing/telemetry.js +66 -3
package/src/api/router.js
CHANGED
|
@@ -131,8 +131,12 @@ async function pickTierByIntent(body) {
|
|
|
131
131
|
const textBearingMsgs = allUserMsgs.filter(
|
|
132
132
|
(m) => extractCleanUserText({ messages: [m] })
|
|
133
133
|
);
|
|
134
|
-
|
|
134
|
+
let windowUserMsgs = (textBearingMsgs.length > 0 ? textBearingMsgs : allUserMsgs)
|
|
135
135
|
.slice(-N); // chronological, oldest-first
|
|
136
|
+
try {
|
|
137
|
+
const _hAsk = require("../routing/harness-envelope").harnessAskFromPayload(body);
|
|
138
|
+
if (_hAsk?.text) windowUserMsgs = [{ role: 'user', content: _hAsk.text }];
|
|
139
|
+
} catch { /* fall back to the sliding window */ }
|
|
136
140
|
|
|
137
141
|
// WS3 — we USED to slice tools to 3 here so Claude Code's 11 baseline
|
|
138
142
|
// tools didn't inflate the agentic detector's tool-count signal. That
|
|
@@ -284,6 +288,21 @@ async function pickTierByIntent(body) {
|
|
|
284
288
|
perMsg: perMsgScores,
|
|
285
289
|
}, "[OAuthIntent] window scoring decision");
|
|
286
290
|
|
|
291
|
+
// Decision engine must see the ORIGINAL request (harness preamble, tools,
|
|
292
|
+
// session context), not the cleaned single message the window loop scored —
|
|
293
|
+
// the per-message pass strips the envelope the harness signal keys on.
|
|
294
|
+
let _engineOnBody = d.engine || null;
|
|
295
|
+
try {
|
|
296
|
+
const _dec = require("../routing/decisions");
|
|
297
|
+
const _sid = body?._sessionId || null;
|
|
298
|
+
_engineOnBody = await _dec.evaluate({
|
|
299
|
+
payload: body, analysis: d.analysis || {}, risk: d.risk || null, agenticResult: d.agenticResult || null,
|
|
300
|
+
legacy: { tier: d.tier || null, provider: d.provider, model: d.model || null },
|
|
301
|
+
sessionId: _sid, prevTurns: _sid ? require("../routing/outcomes").ring(_sid) : [],
|
|
302
|
+
});
|
|
303
|
+
} catch (err) {
|
|
304
|
+
logger.debug({ err: err.message }, "[OAuthIntent] decision engine on body failed — keeping per-message result");
|
|
305
|
+
}
|
|
287
306
|
return {
|
|
288
307
|
tier: d.tier || null,
|
|
289
308
|
provider: d.provider,
|
|
@@ -307,6 +326,7 @@ async function pickTierByIntent(body) {
|
|
|
307
326
|
// Underscored: stripped at the outbound chokepoint with every other
|
|
308
327
|
// internal field, never leaks upstream or to headers.
|
|
309
328
|
_jev: (d.analysis && d.analysis.jev) || d.jev || null,
|
|
329
|
+
_engine: _engineOnBody,
|
|
310
330
|
// WS5: feedback path needs the bandit context vector (to call
|
|
311
331
|
// bandit.update with the same features the arm was scored on) and the
|
|
312
332
|
// query embedding (to add conclusive-quality outcomes to kNN). Both
|
|
@@ -1192,6 +1212,21 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
|
|
|
1192
1212
|
// assignment is a no-op when the value already matches.
|
|
1193
1213
|
if (req.sessionId && !req.body._sessionId) {
|
|
1194
1214
|
req.body._sessionId = req.sessionId;
|
|
1215
|
+
// Turn-outcome attribution: classify what happened after the previous
|
|
1216
|
+
// turn of this session from the evidence this request carries.
|
|
1217
|
+
try {
|
|
1218
|
+
const _outcomes = require("../routing/outcomes");
|
|
1219
|
+
const _tlm = require("../routing/telemetry");
|
|
1220
|
+
const _prevRow = typeof _tlm.lastForSession === 'function' ? _tlm.lastForSession(req.sessionId) : null;
|
|
1221
|
+
const _prevRecord = _prevRow ? {
|
|
1222
|
+
statusCode: _prevRow.status_code, errorType: _prevRow.error_type, tier: _prevRow.tier, servedModel: _prevRow.model,
|
|
1223
|
+
failover: /fallback/i.test(String(_prevRow.routing_method || '')) || !!_prevRow.was_fallback, tierFallback: !!_prevRow.was_fallback,
|
|
1224
|
+
} : null;
|
|
1225
|
+
const _po = _outcomes.classifyPrevious({ sessionId: req.sessionId, payload: req.body, prevRecord: _prevRecord });
|
|
1226
|
+
if (_po) req.body._prevOutcome = _po;
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
logger.debug({ err: err.message }, '[Outcomes] classification failed (ignored)');
|
|
1229
|
+
}
|
|
1195
1230
|
}
|
|
1196
1231
|
|
|
1197
1232
|
// TaskBand — fold the thread (minus the current ask) into a task ledger
|
|
@@ -1422,7 +1457,7 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
|
|
|
1422
1457
|
// reached the risk classifier. Require a detected client profile
|
|
1423
1458
|
// (harness UA / tool fingerprint) before treating tool-less traffic as
|
|
1424
1459
|
// side traffic; a suggestion-mode tag is harness evidence by itself.
|
|
1425
|
-
const isKnownHarness = !!req.body?._clientProfile;
|
|
1460
|
+
const isKnownHarness = !!req.body?._clientProfile && req.body._clientProfile.toolless !== true;
|
|
1426
1461
|
// Signal 1 — message-count regression. Real turns grow the transcript
|
|
1427
1462
|
// monotonically; a harness replay (title-gen, recap, summary) truncates
|
|
1428
1463
|
// the history down to the wrapper prompt. If this payload has fewer
|
|
@@ -1540,11 +1575,12 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
|
|
|
1540
1575
|
let _pinForceBypass = null;
|
|
1541
1576
|
if (!sideTier && pinCheck.serve && pinCheck.reason === 'guards_passed' && !isSideRequest) {
|
|
1542
1577
|
try {
|
|
1543
|
-
const
|
|
1578
|
+
const _harnessAskForPin = require("../routing/harness-envelope").harnessAskFromPayload(req.body);
|
|
1579
|
+
const _probe = { messages: [{ role: 'user', content: (_harnessAskForPin ? _harnessAskForPin.text : _lastUserAskClean) || '' }] };
|
|
1544
1580
|
const ca = require("../routing/complexity-analyzer");
|
|
1545
1581
|
if (_lastUserText && ca.shouldForceReasoning(_probe)) _pinForceBypass = 'force_reasoning';
|
|
1546
1582
|
else if (_lastUserText && ca.shouldForceCloud(_probe)) _pinForceBypass = 'force_cloud';
|
|
1547
|
-
else if (_lastUserText) {
|
|
1583
|
+
else if (_lastUserText && process.env.RISK_TIER_ESCALATION !== 'false') {
|
|
1548
1584
|
const _pinRisk = analyzeRisk(_probe);
|
|
1549
1585
|
if (_pinRisk?.level === 'high') _pinForceBypass = 'risk_high';
|
|
1550
1586
|
}
|
|
@@ -2042,6 +2078,16 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
|
|
|
2042
2078
|
// WS4 — propensity + candidates land on every telemetry row so downstream
|
|
2043
2079
|
// off-policy evaluation can score any counterfactual policy from logs.
|
|
2044
2080
|
if (tier.propensity != null) req.body._propensity = tier.propensity;
|
|
2081
|
+
if (tier._jev && typeof tier._jev === 'object') req.body._jev = tier._jev;
|
|
2082
|
+
// Declarative decision engine result + per-decision effort travel with the body.
|
|
2083
|
+
const _eng = tier._engine || tier.engine || null;
|
|
2084
|
+
if (_eng) {
|
|
2085
|
+
req.body._engine = { decision: _eng.decision, tier: _eng.tier, mode: _eng.mode, effort: _eng.effort, hosts: _eng.hosts, agreesWithLegacy: _eng.agreesWithLegacy, cascade: _eng.cascade || null };
|
|
2086
|
+
// Effort is a SERVING change: apply it only when the engine is allowed
|
|
2087
|
+
// to serve (enforce mode) or when its tier is the one actually served.
|
|
2088
|
+
if (_eng.effort && (_eng.mode === 'enforce' || _eng.tier === tier.tier)) req.body._effort = _eng.effort;
|
|
2089
|
+
}
|
|
2090
|
+
if (pinCheck && pinCheck.gate) req.body._gate = { action: pinCheck.gate.action, reason: pinCheck.gate.reason, target: pinCheck.gate.target, enforced: pinCheck.gate.enforced, streak: pinCheck.gate.streak };
|
|
2045
2091
|
if (tier.candidates) req.body._candidates = tier.candidates;
|
|
2046
2092
|
// WS5 — bandit context vector + query embedding for the feedback loop.
|
|
2047
2093
|
// All three are underscored; `_stripInternalFields` scrubs them before
|
|
@@ -2185,6 +2231,9 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
|
|
|
2185
2231
|
};
|
|
2186
2232
|
|
|
2187
2233
|
const routingHeaders = getRoutingHeaders(preRouteDecision);
|
|
2234
|
+
if (process.env.LYNKR_DECISION_HEADERS === 'true' && req._intentTier?._engine) {
|
|
2235
|
+
try { Object.assign(routingHeaders, require("../routing/decisions").headerSummary(req._intentTier._engine)); } catch { /* headers are best-effort */ }
|
|
2236
|
+
}
|
|
2188
2237
|
|
|
2189
2238
|
// Build the interaction block once. It travels in headers always
|
|
2190
2239
|
// (X-Lynkr-Interaction-* derived fields) and optionally into the
|
package/src/cache/embeddings.js
CHANGED
|
@@ -29,7 +29,9 @@ async function generateOllamaEmbedding(text) {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
if (!response.ok) {
|
|
32
|
-
|
|
32
|
+
let body = '';
|
|
33
|
+
try { body = (await response.text()).slice(0, 300); } catch { /* ignore */ }
|
|
34
|
+
throw new Error(`Ollama embedding failed: ${response.status} ${response.statusText}${body ? ' — ' + body : ''}`);
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
const data = await response.json();
|
|
@@ -178,6 +180,10 @@ function _noteRecovery(providerName) {
|
|
|
178
180
|
// absorbs the blip; a provider that is actually down fails twice and
|
|
179
181
|
// degrades exactly as before.
|
|
180
182
|
const TRANSIENT_RETRY_DELAY_MS = 1500;
|
|
183
|
+
const _INPUT_LENGTH_RE = /context length|input length|too long|exceeds? (the )?(maximum|context)|maximum context/i;
|
|
184
|
+
function _isInputLengthError(err) {
|
|
185
|
+
return _INPUT_LENGTH_RE.test(String(err?.message || err || ''));
|
|
186
|
+
}
|
|
181
187
|
|
|
182
188
|
function _wrapProvider(providerName, providerFn) {
|
|
183
189
|
return async (text) => {
|
|
@@ -196,6 +202,20 @@ function _wrapProvider(providerName, providerFn) {
|
|
|
196
202
|
_noteRecovery(providerName);
|
|
197
203
|
return result;
|
|
198
204
|
} catch (firstErr) {
|
|
205
|
+
if (_isInputLengthError(firstErr)) {
|
|
206
|
+
try {
|
|
207
|
+
const result = await providerFn(text.slice(0, Math.max(256, Math.floor(text.length / 2))));
|
|
208
|
+
logger.debug({ provider: providerName, chars: text.length },
|
|
209
|
+
'[Embeddings] Input exceeded model context — embedded truncated half instead');
|
|
210
|
+
return result;
|
|
211
|
+
} catch (lenErr) {
|
|
212
|
+
logger.warn({ provider: providerName, error: lenErr?.message, chars: text.length },
|
|
213
|
+
'[Embeddings] Input-length rejection persisted after halving — hash fallback for this request only');
|
|
214
|
+
fallbackCount += 1;
|
|
215
|
+
if (STRICT) throw lenErr;
|
|
216
|
+
return generateHashEmbedding(text);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
199
219
|
// Already-degraded providers get no second chance (this attempt WAS the
|
|
200
220
|
// post-cooldown probe); healthy ones earn one retry before the flip.
|
|
201
221
|
if (embeddingProviderAvailable !== false) {
|
|
@@ -282,8 +302,7 @@ async function generateEmbedding(text) {
|
|
|
282
302
|
throw new Error('Cannot generate embedding for empty text');
|
|
283
303
|
}
|
|
284
304
|
|
|
285
|
-
|
|
286
|
-
const maxLength = 8000;
|
|
305
|
+
const maxLength = Number.parseInt(process.env.LYNKR_EMBEDDINGS_MAX_CHARS, 10) || 5000;
|
|
287
306
|
const truncated = text.length > maxLength ? text.substring(0, maxLength) : text;
|
|
288
307
|
|
|
289
308
|
const embedFn = getEmbeddingFunction();
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* @module clients/cursor-utils
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
-
const { execFile
|
|
34
|
+
const { execFile } = require("node:child_process");
|
|
35
35
|
const crypto = require("node:crypto");
|
|
36
36
|
const fs = require("node:fs");
|
|
37
37
|
const os = require("node:os");
|