@juspay/neurolink 12.18.0 → 12.19.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/CHANGELOG.md +2 -2
- package/README.md +277 -75
- package/dist/browser/neurolink.min.js +396 -396
- package/dist/cli/commands/setup.js +2 -1
- package/dist/cli/factories/commandFactory.d.ts +22 -2
- package/dist/cli/factories/commandFactory.js +14 -2
- package/dist/cli/loop/optionsSchema.js +4 -0
- package/dist/cli/utils/classifierRouterFlags.js +39 -1
- package/dist/constants/enums.d.ts +13 -0
- package/dist/constants/enums.js +14 -0
- package/dist/context/budgetChecker.d.ts +3 -1
- package/dist/context/budgetChecker.js +20 -4
- package/dist/context/contextCompactor.d.ts +43 -3
- package/dist/context/contextCompactor.js +157 -41
- package/dist/context/contextDecision.d.ts +56 -0
- package/dist/context/contextDecision.js +234 -0
- package/dist/core/baseProvider.d.ts +27 -1
- package/dist/core/baseProvider.js +39 -0
- package/dist/core/toolRouting.d.ts +8 -0
- package/dist/core/toolRouting.js +52 -1
- package/dist/core/toolRoutingDecision.d.ts +35 -0
- package/dist/core/toolRoutingDecision.js +194 -0
- package/dist/factories/providerDescriptors.d.ts +15 -0
- package/dist/factories/providerDescriptors.js +57 -1
- package/dist/factories/providerRegistry.js +9 -1
- package/dist/index.d.ts +57 -1
- package/dist/index.js +60 -1
- package/dist/models/manifestRegistry.js +2 -0
- package/dist/models/manifests/typesafe.d.ts +16 -0
- package/dist/models/manifests/typesafe.js +42 -0
- package/dist/neurolink.d.ts +57 -1
- package/dist/neurolink.js +254 -10
- package/dist/observability/exporters/laminarExporter.js +3 -0
- package/dist/observability/exporters/posthogExporter.js +3 -0
- package/dist/observability/utils/spanSerializer.js +4 -0
- package/dist/providers/catalog/schema.d.ts +1 -1
- package/dist/providers/typesafe.d.ts +112 -0
- package/dist/providers/typesafe.js +654 -0
- package/dist/proxy/proxyHistoryTruncation.js +15 -10
- package/dist/rag/pipeline/RAGPipeline.d.ts +3 -1
- package/dist/rag/pipeline/RAGPipeline.js +65 -4
- package/dist/rag/retrieval/searchDecision.d.ts +27 -0
- package/dist/rag/retrieval/searchDecision.js +124 -0
- package/dist/routing/classifierRouter.d.ts +74 -2
- package/dist/routing/classifierRouter.js +232 -32
- package/dist/routing/classifierStrategies.d.ts +33 -1
- package/dist/routing/classifierStrategies.js +237 -0
- package/dist/routing/index.d.ts +1 -1
- package/dist/routing/index.js +1 -1
- package/dist/routing/modelCatalog.d.ts +79 -0
- package/dist/routing/modelCatalog.js +382 -0
- package/dist/telemetry/tracers.d.ts +1 -0
- package/dist/telemetry/tracers.js +1 -0
- package/dist/types/classifierRouter.d.ts +160 -4
- package/dist/types/cli.d.ts +17 -1
- package/dist/types/config.d.ts +12 -0
- package/dist/types/context.d.ts +55 -1
- package/dist/types/decision.d.ts +200 -0
- package/dist/types/decision.js +21 -0
- package/dist/types/generate.d.ts +20 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/providers.d.ts +59 -1
- package/dist/types/providers.js +5 -0
- package/dist/types/rag.d.ts +48 -0
- package/dist/types/span.d.ts +11 -0
- package/dist/types/span.js +11 -0
- package/dist/types/stream.d.ts +13 -0
- package/dist/types/toolRouting.d.ts +56 -0
- package/dist/utils/decisionAnswers.d.ts +56 -0
- package/dist/utils/decisionAnswers.js +95 -0
- package/dist/utils/modelChoices.js +13 -1
- package/dist/utils/pricing.js +11 -0
- package/dist/utils/providerConfig.d.ts +4 -0
- package/dist/utils/providerConfig.js +17 -0
- package/docs-site/static/search-index.json +1 -1
- package/package.json +16 -3
|
@@ -20,7 +20,7 @@ import { handleGCPSetup } from "./setup-gcp.js";
|
|
|
20
20
|
import { handleHuggingFaceSetup } from "./setup-huggingface.js";
|
|
21
21
|
import { handleMistralSetup } from "./setup-mistral.js";
|
|
22
22
|
import { PROVIDER_DESCRIPTORS_BY_NAME } from "../../factories/providerDescriptors.js";
|
|
23
|
-
import { createCohereConfig, createDeepSeekConfig, createIdeogramConfig, createJinaConfig, createNvidiaNimConfig, createOpenAICompatibleConfig, createRecraftConfig, createReplicateConfig, createStabilityConfig, createVoyageConfig, satisfiesFallbacks, } from "../../utils/providerConfig.js";
|
|
23
|
+
import { createCohereConfig, createDeepSeekConfig, createIdeogramConfig, createJinaConfig, createNvidiaNimConfig, createOpenAICompatibleConfig, createRecraftConfig, createReplicateConfig, createStabilityConfig, createVoyageConfig, createTypeSafeConfig, satisfiesFallbacks, } from "../../utils/providerConfig.js";
|
|
24
24
|
import { getCatalogJsonEntries, buildCatalogConfigOptions, } from "../../providers/catalog/loader.js";
|
|
25
25
|
// Provider information database
|
|
26
26
|
const PROVIDERS = [
|
|
@@ -168,6 +168,7 @@ export const EXTRA_PROVIDER_CONFIGS = {
|
|
|
168
168
|
stability: createStabilityConfig(),
|
|
169
169
|
ideogram: createIdeogramConfig(),
|
|
170
170
|
recraft: createRecraftConfig(),
|
|
171
|
+
typesafe: createTypeSafeConfig(),
|
|
171
172
|
...Object.fromEntries(getCatalogJsonEntries()
|
|
172
173
|
.filter((e) => e.id !== "mistral")
|
|
173
174
|
.map((e) => [e.id, buildCatalogConfigOptions(e)])),
|
|
@@ -486,7 +486,17 @@ export declare class CLICommandFactory {
|
|
|
486
486
|
classifierStrategy: {
|
|
487
487
|
type: "string";
|
|
488
488
|
description: string;
|
|
489
|
-
choices: readonly ["heuristic", "llm"];
|
|
489
|
+
choices: readonly ["auto", "heuristic", "llm", "jev"];
|
|
490
|
+
alias: string;
|
|
491
|
+
};
|
|
492
|
+
classifierMinUpgradeConfidence: {
|
|
493
|
+
type: "number";
|
|
494
|
+
description: string;
|
|
495
|
+
alias: string;
|
|
496
|
+
};
|
|
497
|
+
classifierMinDowngradeConfidence: {
|
|
498
|
+
type: "number";
|
|
499
|
+
description: string;
|
|
490
500
|
alias: string;
|
|
491
501
|
};
|
|
492
502
|
classifierModelProvider: {
|
|
@@ -1379,7 +1389,17 @@ export declare const commonOptions: {
|
|
|
1379
1389
|
classifierStrategy: {
|
|
1380
1390
|
type: "string";
|
|
1381
1391
|
description: string;
|
|
1382
|
-
choices: readonly ["heuristic", "llm"];
|
|
1392
|
+
choices: readonly ["auto", "heuristic", "llm", "jev"];
|
|
1393
|
+
alias: string;
|
|
1394
|
+
};
|
|
1395
|
+
classifierMinUpgradeConfidence: {
|
|
1396
|
+
type: "number";
|
|
1397
|
+
description: string;
|
|
1398
|
+
alias: string;
|
|
1399
|
+
};
|
|
1400
|
+
classifierMinDowngradeConfidence: {
|
|
1401
|
+
type: "number";
|
|
1402
|
+
description: string;
|
|
1383
1403
|
alias: string;
|
|
1384
1404
|
};
|
|
1385
1405
|
classifierModelProvider: {
|
|
@@ -592,10 +592,20 @@ export class CLICommandFactory {
|
|
|
592
592
|
},
|
|
593
593
|
classifierStrategy: {
|
|
594
594
|
type: "string",
|
|
595
|
-
description: "Classifier strategy: '
|
|
596
|
-
choices: ["heuristic", "llm"],
|
|
595
|
+
description: "Classifier strategy: 'auto' (default — 'jev' when TYPESAFE_API_KEY is set, else 'heuristic'), 'heuristic' (no LLM), 'llm' (a cheap model picks per prompt), or 'jev' (TypeSafe System One, ~400ms with calibrated confidence).",
|
|
596
|
+
choices: ["auto", "heuristic", "llm", "jev"],
|
|
597
597
|
alias: "classifier-strategy",
|
|
598
598
|
},
|
|
599
|
+
classifierMinUpgradeConfidence: {
|
|
600
|
+
type: "number",
|
|
601
|
+
description: "Minimum confidence to route UP to a costlier model; below it the heuristic is used. Only meaningful for 'jev'. Default: 0.3.",
|
|
602
|
+
alias: "classifier-min-upgrade-confidence",
|
|
603
|
+
},
|
|
604
|
+
classifierMinDowngradeConfidence: {
|
|
605
|
+
type: "number",
|
|
606
|
+
description: "Minimum confidence to route DOWN to a cheaper model. Higher than the upgrade bar, because the two mistakes cost differently. Default: 0.6.",
|
|
607
|
+
alias: "classifier-min-downgrade-confidence",
|
|
608
|
+
},
|
|
599
609
|
classifierModelProvider: {
|
|
600
610
|
type: "string",
|
|
601
611
|
description: "Provider for the LLM classifier model (strategy=llm).",
|
|
@@ -959,6 +969,8 @@ export class CLICommandFactory {
|
|
|
959
969
|
// Classifier-router flags — constructor-level config (see note above).
|
|
960
970
|
classifierRouter: argv.classifierRouter,
|
|
961
971
|
classifierStrategy: argv.classifierStrategy,
|
|
972
|
+
classifierMinUpgradeConfidence: argv.classifierMinUpgradeConfidence,
|
|
973
|
+
classifierMinDowngradeConfidence: argv.classifierMinDowngradeConfidence,
|
|
962
974
|
classifierModelProvider: argv.classifierModelProvider,
|
|
963
975
|
classifierModelName: argv.classifierModelName,
|
|
964
976
|
classifierModelRegion: argv.classifierModelRegion,
|
|
@@ -24,6 +24,10 @@ export const textGenerationOptionsSchema = {
|
|
|
24
24
|
type: "number",
|
|
25
25
|
description: "The maximum number of tokens to generate.",
|
|
26
26
|
},
|
|
27
|
+
compactionThreshold: {
|
|
28
|
+
type: "number",
|
|
29
|
+
description: "Fraction of the context window (0-1) at which history is compacted. Default 0.8.",
|
|
30
|
+
},
|
|
27
31
|
topP: {
|
|
28
32
|
type: "number",
|
|
29
33
|
description: "Top-p (nucleus) sampling parameter. Controls diversity of generated tokens (0.0-1.0).",
|
|
@@ -15,6 +15,13 @@ import { logger } from "../../utils/logger.js";
|
|
|
15
15
|
// boundary.
|
|
16
16
|
const MAX_POOL_INPUT_BYTES = 1_000_000; // 1 MB
|
|
17
17
|
const MAX_POOL_ENTRIES = 1_000;
|
|
18
|
+
/** Narrow a raw flag string onto the strategy union without an assertion. */
|
|
19
|
+
function isStrategyKind(value) {
|
|
20
|
+
return (value === "heuristic" ||
|
|
21
|
+
value === "llm" ||
|
|
22
|
+
value === "jev" ||
|
|
23
|
+
value === "auto");
|
|
24
|
+
}
|
|
18
25
|
/**
|
|
19
26
|
* Build a {@link ClassifierRouterConfig} from the parsed CLI flags.
|
|
20
27
|
*
|
|
@@ -30,7 +37,12 @@ export function buildClassifierRouterConfigFromCli(flags) {
|
|
|
30
37
|
if (!flags.classifierRouter) {
|
|
31
38
|
return undefined;
|
|
32
39
|
}
|
|
33
|
-
|
|
40
|
+
// Defaults to "auto", which resolves to "jev" when TYPESAFE_API_KEY is set
|
|
41
|
+
// and "heuristic" otherwise — so a configured key upgrades CLI routing with
|
|
42
|
+
// no flag change, and an unset one behaves exactly as before.
|
|
43
|
+
const strategy = isStrategyKind(flags.classifierStrategy)
|
|
44
|
+
? flags.classifierStrategy
|
|
45
|
+
: "auto";
|
|
34
46
|
const pool = typeof flags.classifierPool === "string" &&
|
|
35
47
|
flags.classifierPool.trim() !== ""
|
|
36
48
|
? parsePoolFlag(flags.classifierPool)
|
|
@@ -52,6 +64,32 @@ export function buildClassifierRouterConfigFromCli(flags) {
|
|
|
52
64
|
logger.warn(`[classifier-router] --classifier-timeout value ${String(t)} is not a positive finite number; ignoring (SDK default applies).`);
|
|
53
65
|
}
|
|
54
66
|
}
|
|
67
|
+
// Values above 1 are accepted deliberately: an unreachable floor is a
|
|
68
|
+
// legitimate way to force the heuristic while leaving the strategy set.
|
|
69
|
+
const confidenceFlags = [
|
|
70
|
+
[
|
|
71
|
+
"classifierMinUpgradeConfidence",
|
|
72
|
+
"minUpgradeConfidence",
|
|
73
|
+
"--classifier-min-upgrade-confidence",
|
|
74
|
+
],
|
|
75
|
+
[
|
|
76
|
+
"classifierMinDowngradeConfidence",
|
|
77
|
+
"minDowngradeConfidence",
|
|
78
|
+
"--classifier-min-downgrade-confidence",
|
|
79
|
+
],
|
|
80
|
+
];
|
|
81
|
+
for (const [flagKey, configKey, cliName] of confidenceFlags) {
|
|
82
|
+
const value = flags[flagKey];
|
|
83
|
+
if (value === undefined) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
87
|
+
config[configKey] = value;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
logger.warn(`[classifier-router] ${cliName} value ${String(value)} is not a non-negative finite number; ignoring (SDK default applies).`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
55
93
|
const hasProvider = typeof flags.classifierModelProvider === "string";
|
|
56
94
|
const hasModel = typeof flags.classifierModelName === "string";
|
|
57
95
|
const hasRegion = typeof flags.classifierModelRegion === "string";
|
|
@@ -41,6 +41,8 @@ export declare enum AIProviderName {
|
|
|
41
41
|
STABILITY = "stability",
|
|
42
42
|
IDEOGRAM = "ideogram",
|
|
43
43
|
RECRAFT = "recraft",
|
|
44
|
+
/** TypeSafe (Jev) — serves the `decide` inference type only. */
|
|
45
|
+
TYPESAFE = "typesafe",
|
|
44
46
|
AUTO = "auto"
|
|
45
47
|
}
|
|
46
48
|
/**
|
|
@@ -1030,3 +1032,14 @@ export declare enum XaiModels {
|
|
|
1030
1032
|
GROK_2_VISION_LATEST = "grok-2-vision-latest",
|
|
1031
1033
|
GROK_BETA = "grok-beta"
|
|
1032
1034
|
}
|
|
1035
|
+
/**
|
|
1036
|
+
* TypeSafe decision models. Hand-written: TypeSafe is a Tier-3 provider, so
|
|
1037
|
+
* it is not in the provider catalog and codegen never touches this.
|
|
1038
|
+
*
|
|
1039
|
+
* `jev-preview` currently resolves to the same build as `jev-latest`; both
|
|
1040
|
+
* report themselves as a pinned version (e.g. `jev-1.13.0`) in the response.
|
|
1041
|
+
*/
|
|
1042
|
+
export declare enum TypeSafeModels {
|
|
1043
|
+
JEV_LATEST = "jev-latest",
|
|
1044
|
+
JEV_PREVIEW = "jev-preview"
|
|
1045
|
+
}
|
package/dist/constants/enums.js
CHANGED
|
@@ -47,6 +47,8 @@ export var AIProviderName;
|
|
|
47
47
|
AIProviderName["STABILITY"] = "stability";
|
|
48
48
|
AIProviderName["IDEOGRAM"] = "ideogram";
|
|
49
49
|
AIProviderName["RECRAFT"] = "recraft";
|
|
50
|
+
/** TypeSafe (Jev) — serves the `decide` inference type only. */
|
|
51
|
+
AIProviderName["TYPESAFE"] = "typesafe";
|
|
50
52
|
AIProviderName["AUTO"] = "auto";
|
|
51
53
|
})(AIProviderName || (AIProviderName = {}));
|
|
52
54
|
/**
|
|
@@ -1301,3 +1303,15 @@ export var XaiModels;
|
|
|
1301
1303
|
XaiModels["GROK_BETA"] = "grok-beta";
|
|
1302
1304
|
})(XaiModels || (XaiModels = {}));
|
|
1303
1305
|
// ── END GENERATED(models-enums) ──
|
|
1306
|
+
/**
|
|
1307
|
+
* TypeSafe decision models. Hand-written: TypeSafe is a Tier-3 provider, so
|
|
1308
|
+
* it is not in the provider catalog and codegen never touches this.
|
|
1309
|
+
*
|
|
1310
|
+
* `jev-preview` currently resolves to the same build as `jev-latest`; both
|
|
1311
|
+
* report themselves as a pinned version (e.g. `jev-1.13.0`) in the response.
|
|
1312
|
+
*/
|
|
1313
|
+
export var TypeSafeModels;
|
|
1314
|
+
(function (TypeSafeModels) {
|
|
1315
|
+
TypeSafeModels["JEV_LATEST"] = "jev-latest";
|
|
1316
|
+
TypeSafeModels["JEV_PREVIEW"] = "jev-preview";
|
|
1317
|
+
})(TypeSafeModels || (TypeSafeModels = {}));
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* This runs BEFORE every LLM call to prevent context overflow.
|
|
8
8
|
*/
|
|
9
9
|
import type { BudgetCheckResult, BudgetCheckParams } from "../types/index.js";
|
|
10
|
+
/** Default compaction threshold (80% of available input) */
|
|
11
|
+
export declare const DEFAULT_COMPACTION_THRESHOLD = 0.8;
|
|
10
12
|
/**
|
|
11
13
|
* Tokens the CONVERSATION HISTORY may occupy, i.e. the model's available input
|
|
12
14
|
* space minus everything that rides alongside it (system prompt, current
|
|
@@ -22,7 +24,7 @@ import type { BudgetCheckResult, BudgetCheckParams } from "../types/index.js";
|
|
|
22
24
|
* Returns 0 when the fixed overhead already exceeds the window; callers must
|
|
23
25
|
* treat that as unrecoverable rather than compacting to an empty history.
|
|
24
26
|
*/
|
|
25
|
-
export declare function resolveHistoryBudget(result: BudgetCheckResult): number;
|
|
27
|
+
export declare function resolveHistoryBudget(result: BudgetCheckResult, compactionThreshold?: number): number;
|
|
26
28
|
/**
|
|
27
29
|
* Check whether a request fits within the model's context budget.
|
|
28
30
|
*
|
|
@@ -11,7 +11,7 @@ import { estimateMessagesTokens, estimateTokens, TOKENS_PER_MESSAGE, } from "../
|
|
|
11
11
|
import { SpanSerializer, SpanType, SpanStatus, getMetricsAggregator, } from "../observability/index.js";
|
|
12
12
|
import { getActiveTraceContext } from "../telemetry/traceContext.js";
|
|
13
13
|
/** Default compaction threshold (80% of available input) */
|
|
14
|
-
const DEFAULT_COMPACTION_THRESHOLD = 0.8;
|
|
14
|
+
export const DEFAULT_COMPACTION_THRESHOLD = 0.8;
|
|
15
15
|
/**
|
|
16
16
|
* Fraction of the derived history budget actually handed to the compactor.
|
|
17
17
|
* Estimation is char-based and approximate, so the compactor aims slightly
|
|
@@ -35,16 +35,27 @@ const TOKENS_PER_TOOL_DEFINITION = 200;
|
|
|
35
35
|
* Returns 0 when the fixed overhead already exceeds the window; callers must
|
|
36
36
|
* treat that as unrecoverable rather than compacting to an empty history.
|
|
37
37
|
*/
|
|
38
|
-
export function resolveHistoryBudget(result) {
|
|
38
|
+
export function resolveHistoryBudget(result, compactionThreshold) {
|
|
39
39
|
const { availableInputTokens, breakdown } = result;
|
|
40
|
+
// A per-request threshold says how much of the window this request should
|
|
41
|
+
// occupy. Expressed against the 0.8 default it becomes a pure scale factor,
|
|
42
|
+
// so the default reproduces the previous number EXACTLY (0.8/0.8 = 1) and
|
|
43
|
+
// anything lower shrinks the target. Clamped to at most 1 because this is
|
|
44
|
+
// the "give the model less context" lever, never the "give it more" one —
|
|
45
|
+
// over-filling a window is a hard provider error that ModelPool records as
|
|
46
|
+
// a permanent cooldown.
|
|
47
|
+
const scale = compactionThreshold === undefined
|
|
48
|
+
? 1
|
|
49
|
+
: Math.max(0, Math.min(1, compactionThreshold / DEFAULT_COMPACTION_THRESHOLD));
|
|
50
|
+
const factor = HISTORY_BUDGET_SAFETY_FACTOR * scale;
|
|
40
51
|
if (!breakdown) {
|
|
41
|
-
return Math.max(0, Math.floor(availableInputTokens *
|
|
52
|
+
return Math.max(0, Math.floor(availableInputTokens * factor));
|
|
42
53
|
}
|
|
43
54
|
const overhead = breakdown.systemPrompt +
|
|
44
55
|
breakdown.currentPrompt +
|
|
45
56
|
breakdown.toolDefinitions +
|
|
46
57
|
breakdown.fileAttachments;
|
|
47
|
-
return Math.max(0, Math.floor((availableInputTokens - overhead) *
|
|
58
|
+
return Math.max(0, Math.floor((availableInputTokens - overhead) * factor));
|
|
48
59
|
}
|
|
49
60
|
/**
|
|
50
61
|
* Check whether a request fits within the model's context budget.
|
|
@@ -116,6 +127,11 @@ export function checkContextBudget(params) {
|
|
|
116
127
|
"context.triggered": shouldCompact,
|
|
117
128
|
"context.estimatedTokens": estimatedInputTokens,
|
|
118
129
|
"context.availableTokens": availableInputTokens,
|
|
130
|
+
// The threshold this check actually used. `shouldCompact` is
|
|
131
|
+
// meaningless without it once the threshold became per-request:
|
|
132
|
+
// the same usageRatio can be over budget for one request and under
|
|
133
|
+
// it for the next.
|
|
134
|
+
"context.compactionThreshold": compactionThreshold,
|
|
119
135
|
}), SpanStatus.OK);
|
|
120
136
|
getMetricsAggregator().recordSpan(endedSpan);
|
|
121
137
|
return result;
|
|
@@ -8,12 +8,52 @@
|
|
|
8
8
|
* Stage 3: LLM Summarization (expensive -- requires LLM call)
|
|
9
9
|
* Stage 4: Sliding Window Truncation (fallback -- no LLM call)
|
|
10
10
|
*/
|
|
11
|
-
import type { ChatMessage, ConversationMemoryConfig, CompactionResult, CompactionConfig } from "../types/index.js";
|
|
11
|
+
import type { ChatMessage, ConversationMemoryConfig, CompactionResult, CompactionConfig, ContextCompactorDeps, ContextCompactRequest } from "../types/index.js";
|
|
12
12
|
export declare class ContextCompactor {
|
|
13
13
|
private config;
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Injected decision caller and its tuning. Kept OUT of `CompactionConfig`
|
|
16
|
+
* deliberately: that type is `Required<>`-ed into `DEFAULT_CONFIG`, so a
|
|
17
|
+
* function member would need a default implementation, and the compactor
|
|
18
|
+
* must stay usable with no decision provider at all.
|
|
19
|
+
*/
|
|
20
|
+
private readonly deps;
|
|
21
|
+
constructor(config?: CompactionConfig, deps?: ContextCompactorDeps);
|
|
15
22
|
/**
|
|
16
23
|
* Run the multi-stage compaction pipeline until messages fit within budget.
|
|
17
24
|
*/
|
|
18
|
-
compact(messages: ChatMessage[], targetTokens: number, memoryConfig?: Partial<ConversationMemoryConfig>, requestId?: string): Promise<CompactionResult>;
|
|
25
|
+
compact(messages: ChatMessage[], targetTokens: number, memoryConfig?: Partial<ConversationMemoryConfig>, requestId?: string, request?: ContextCompactRequest): Promise<CompactionResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Stage 0 — drop the earlier messages this request provably does not need.
|
|
28
|
+
*
|
|
29
|
+
* Returns the surviving messages, or `null` when nothing changed: no
|
|
30
|
+
* decision provider, no known request, nothing eligible, or no confident
|
|
31
|
+
* drop. Never throws — a failure here must leave the positional stages to
|
|
32
|
+
* run exactly as they did before this stage existed.
|
|
33
|
+
*/
|
|
34
|
+
private runRelevanceStage;
|
|
35
|
+
/**
|
|
36
|
+
* Whether a generated summary may replace the messages it covers.
|
|
37
|
+
*
|
|
38
|
+
* Stage 3 accepts any non-empty string today, so a refusal, an error
|
|
39
|
+
* message or a truncated fragment silently destroys the conversation it was
|
|
40
|
+
* meant to preserve — and the originals are gone before anyone reads it.
|
|
41
|
+
*
|
|
42
|
+
* Returns true unless a decision model CONFIDENTLY rejects the summary,
|
|
43
|
+
* because rejecting one falls through to truncation, which loses strictly
|
|
44
|
+
* more than an imperfect summary does.
|
|
45
|
+
*/
|
|
46
|
+
private acceptSummary;
|
|
47
|
+
/**
|
|
48
|
+
* Stage 2 — collapse repeated reads of the same file to the last one.
|
|
49
|
+
* Returns the deduplicated messages, or `null` when the stage is disabled,
|
|
50
|
+
* already within budget, or found nothing to collapse.
|
|
51
|
+
*/
|
|
52
|
+
private runDeduplicateStage;
|
|
53
|
+
/**
|
|
54
|
+
* Stage 1 — shrink large tool outputs outside the protected recent window.
|
|
55
|
+
* Returns the pruned messages, or `null` when the stage is disabled,
|
|
56
|
+
* already within budget, or saved nothing worth keeping.
|
|
57
|
+
*/
|
|
58
|
+
private runPruneStage;
|
|
19
59
|
}
|
|
@@ -19,6 +19,7 @@ import { pruneToolOutputs } from "./stages/toolOutputPruner.js";
|
|
|
19
19
|
import { deduplicateFileReads } from "./stages/fileReadDeduplicator.js";
|
|
20
20
|
import { truncateWithSlidingWindow } from "./stages/slidingWindowTruncator.js";
|
|
21
21
|
import { summarizeMessages } from "./stages/structuredSummarizer.js";
|
|
22
|
+
import { selectIrrelevantMessages, summaryPreservesContext, } from "./contextDecision.js";
|
|
22
23
|
const DEFAULT_CONFIG = {
|
|
23
24
|
enablePrune: true,
|
|
24
25
|
enableDeduplicate: true,
|
|
@@ -35,13 +36,21 @@ const DEFAULT_CONFIG = {
|
|
|
35
36
|
};
|
|
36
37
|
export class ContextCompactor {
|
|
37
38
|
config;
|
|
38
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Injected decision caller and its tuning. Kept OUT of `CompactionConfig`
|
|
41
|
+
* deliberately: that type is `Required<>`-ed into `DEFAULT_CONFIG`, so a
|
|
42
|
+
* function member would need a default implementation, and the compactor
|
|
43
|
+
* must stay usable with no decision provider at all.
|
|
44
|
+
*/
|
|
45
|
+
deps;
|
|
46
|
+
constructor(config, deps) {
|
|
39
47
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
48
|
+
this.deps = deps ?? {};
|
|
40
49
|
}
|
|
41
50
|
/**
|
|
42
51
|
* Run the multi-stage compaction pipeline until messages fit within budget.
|
|
43
52
|
*/
|
|
44
|
-
async compact(messages, targetTokens, memoryConfig, requestId) {
|
|
53
|
+
async compact(messages, targetTokens, memoryConfig, requestId, request) {
|
|
45
54
|
return withSpan({
|
|
46
55
|
name: "neurolink.context.compact",
|
|
47
56
|
tracer: tracers.context,
|
|
@@ -66,46 +75,27 @@ export class ContextCompactor {
|
|
|
66
75
|
estimatedTokens: tokensBefore,
|
|
67
76
|
budgetTokens: targetTokens,
|
|
68
77
|
});
|
|
78
|
+
// Stage 0: Relevance. Runs FIRST, and only when a decision
|
|
79
|
+
// provider is configured and the current request is known — every
|
|
80
|
+
// later stage is positional, so removing what the request provably
|
|
81
|
+
// does not need before they run means they have less to throw away
|
|
82
|
+
// by recency alone. Fails open to exactly the previous pipeline.
|
|
83
|
+
const afterRelevance = await this.runRelevanceStage(currentMessages, provider, targetTokens, request, requestId);
|
|
84
|
+
if (afterRelevance) {
|
|
85
|
+
currentMessages = afterRelevance;
|
|
86
|
+
stagesUsed.push("relevance");
|
|
87
|
+
}
|
|
69
88
|
// Stage 1: Tool Output Pruning
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
protectTokens: this.config.pruneProtectTokens,
|
|
75
|
-
minimumSavings: this.config.pruneMinimumSavings,
|
|
76
|
-
protectedTools: this.config.pruneProtectedTools,
|
|
77
|
-
provider,
|
|
78
|
-
});
|
|
79
|
-
if (pruneResult.pruned) {
|
|
80
|
-
currentMessages = pruneResult.messages;
|
|
81
|
-
stagesUsed.push("prune");
|
|
82
|
-
}
|
|
83
|
-
const stageTokensAfter = estimateMessagesTokens(currentMessages, provider);
|
|
84
|
-
logger.info("[Compaction] Stage 1 (prune)", {
|
|
85
|
-
requestId,
|
|
86
|
-
ran: pruneResult.pruned,
|
|
87
|
-
tokensBefore: stageTokensBefore,
|
|
88
|
-
tokensAfter: stageTokensAfter,
|
|
89
|
-
saved: stageTokensBefore - stageTokensAfter,
|
|
90
|
-
});
|
|
89
|
+
const afterPrune = this.runPruneStage(currentMessages, provider, targetTokens, requestId);
|
|
90
|
+
if (afterPrune) {
|
|
91
|
+
currentMessages = afterPrune;
|
|
92
|
+
stagesUsed.push("prune");
|
|
91
93
|
}
|
|
92
94
|
// Stage 2: File Read Deduplication
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (dedupResult.deduplicated) {
|
|
98
|
-
currentMessages = dedupResult.messages;
|
|
99
|
-
stagesUsed.push("deduplicate");
|
|
100
|
-
}
|
|
101
|
-
const stageTokensAfter = estimateMessagesTokens(currentMessages, provider);
|
|
102
|
-
logger.info("[Compaction] Stage 2 (deduplicate)", {
|
|
103
|
-
requestId,
|
|
104
|
-
ran: dedupResult.deduplicated,
|
|
105
|
-
tokensBefore: stageTokensBefore,
|
|
106
|
-
tokensAfter: stageTokensAfter,
|
|
107
|
-
saved: stageTokensBefore - stageTokensAfter,
|
|
108
|
-
});
|
|
95
|
+
const afterDedup = this.runDeduplicateStage(currentMessages, provider, targetTokens, requestId);
|
|
96
|
+
if (afterDedup) {
|
|
97
|
+
currentMessages = afterDedup;
|
|
98
|
+
stagesUsed.push("deduplicate");
|
|
109
99
|
}
|
|
110
100
|
// Stage 3: LLM Summarization
|
|
111
101
|
if (this.config.enableSummarize &&
|
|
@@ -119,7 +109,16 @@ export class ContextCompactor {
|
|
|
119
109
|
memoryConfig,
|
|
120
110
|
targetTokens,
|
|
121
111
|
}), 120_000, "LLM summarization timed out after 120s");
|
|
122
|
-
|
|
112
|
+
// Gate the summary before it replaces anything — see
|
|
113
|
+
// acceptSummary for why a rejection is deliberately rare.
|
|
114
|
+
const summaryAccepted = summarizeResult.summarized &&
|
|
115
|
+
(await this.acceptSummary(currentMessages, summarizeResult.messages));
|
|
116
|
+
if (summarizeResult.summarized && !summaryAccepted) {
|
|
117
|
+
span = SpanSerializer.updateAttributes(span, {
|
|
118
|
+
"compaction.stage3.summaryRejected": true,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (summaryAccepted) {
|
|
123
122
|
// Pinned skill instructions must survive summarization:
|
|
124
123
|
// re-seat any that the summarized region swallowed right
|
|
125
124
|
// after the summary message (index 0 of the result).
|
|
@@ -140,7 +139,7 @@ export class ContextCompactor {
|
|
|
140
139
|
const stageTokensAfter = estimateMessagesTokens(currentMessages, provider);
|
|
141
140
|
logger.info("[Compaction] Stage 3 (summarize)", {
|
|
142
141
|
requestId,
|
|
143
|
-
ran:
|
|
142
|
+
ran: summaryAccepted,
|
|
144
143
|
tokensBefore: stageTokensBefore,
|
|
145
144
|
tokensAfter: stageTokensAfter,
|
|
146
145
|
saved: stageTokensBefore - stageTokensAfter,
|
|
@@ -254,4 +253,121 @@ export class ContextCompactor {
|
|
|
254
253
|
}
|
|
255
254
|
}); // end withSpan
|
|
256
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Stage 0 — drop the earlier messages this request provably does not need.
|
|
258
|
+
*
|
|
259
|
+
* Returns the surviving messages, or `null` when nothing changed: no
|
|
260
|
+
* decision provider, no known request, nothing eligible, or no confident
|
|
261
|
+
* drop. Never throws — a failure here must leave the positional stages to
|
|
262
|
+
* run exactly as they did before this stage existed.
|
|
263
|
+
*/
|
|
264
|
+
async runRelevanceStage(messages, provider, targetTokens, request, requestId) {
|
|
265
|
+
if (!this.deps.decide || !request?.currentRequest) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
const tokensBefore = estimateMessagesTokens(messages, provider);
|
|
269
|
+
if (tokensBefore <= targetTokens) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
const relevance = await selectIrrelevantMessages(messages, request.currentRequest, this.deps.decide, this.deps.relevance);
|
|
274
|
+
logger.info("[Compaction] Stage 0 (relevance)", {
|
|
275
|
+
requestId,
|
|
276
|
+
ran: !!relevance,
|
|
277
|
+
tokensBefore,
|
|
278
|
+
tokensAfter: relevance
|
|
279
|
+
? estimateMessagesTokens(relevance.messages, provider)
|
|
280
|
+
: tokensBefore,
|
|
281
|
+
droppedCount: relevance?.droppedIndices.length ?? 0,
|
|
282
|
+
});
|
|
283
|
+
return relevance ? relevance.messages : null;
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
logger.warn("[Compaction] Stage 0 (relevance) FAILED", {
|
|
287
|
+
requestId,
|
|
288
|
+
error: error instanceof Error ? error.message : String(error),
|
|
289
|
+
});
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Whether a generated summary may replace the messages it covers.
|
|
295
|
+
*
|
|
296
|
+
* Stage 3 accepts any non-empty string today, so a refusal, an error
|
|
297
|
+
* message or a truncated fragment silently destroys the conversation it was
|
|
298
|
+
* meant to preserve — and the originals are gone before anyone reads it.
|
|
299
|
+
*
|
|
300
|
+
* Returns true unless a decision model CONFIDENTLY rejects the summary,
|
|
301
|
+
* because rejecting one falls through to truncation, which loses strictly
|
|
302
|
+
* more than an imperfect summary does.
|
|
303
|
+
*/
|
|
304
|
+
async acceptSummary(before, after) {
|
|
305
|
+
if (!this.deps.decide) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
const survivingIds = new Set(after.map((m) => m.id));
|
|
309
|
+
const replaced = before.filter((m) => !survivingIds.has(m.id));
|
|
310
|
+
const summaryText = after.find((m) => m.metadata?.isSummary)?.content;
|
|
311
|
+
if (!summaryText || replaced.length === 0) {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
return summaryPreservesContext(summaryText, replaced, this.deps.decide, this.deps.summaryGate);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Stage 2 — collapse repeated reads of the same file to the last one.
|
|
318
|
+
* Returns the deduplicated messages, or `null` when the stage is disabled,
|
|
319
|
+
* already within budget, or found nothing to collapse.
|
|
320
|
+
*/
|
|
321
|
+
runDeduplicateStage(messages, provider, targetTokens, requestId) {
|
|
322
|
+
if (!this.config.enableDeduplicate) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
const tokensBefore = estimateMessagesTokens(messages, provider);
|
|
326
|
+
if (tokensBefore <= targetTokens) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
const dedupResult = deduplicateFileReads(messages);
|
|
330
|
+
const tokensAfter = dedupResult.deduplicated
|
|
331
|
+
? estimateMessagesTokens(dedupResult.messages, provider)
|
|
332
|
+
: tokensBefore;
|
|
333
|
+
logger.info("[Compaction] Stage 2 (deduplicate)", {
|
|
334
|
+
requestId,
|
|
335
|
+
ran: dedupResult.deduplicated,
|
|
336
|
+
tokensBefore,
|
|
337
|
+
tokensAfter,
|
|
338
|
+
saved: tokensBefore - tokensAfter,
|
|
339
|
+
});
|
|
340
|
+
return dedupResult.deduplicated ? dedupResult.messages : null;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Stage 1 — shrink large tool outputs outside the protected recent window.
|
|
344
|
+
* Returns the pruned messages, or `null` when the stage is disabled,
|
|
345
|
+
* already within budget, or saved nothing worth keeping.
|
|
346
|
+
*/
|
|
347
|
+
runPruneStage(messages, provider, targetTokens, requestId) {
|
|
348
|
+
if (!this.config.enablePrune) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
const tokensBefore = estimateMessagesTokens(messages, provider);
|
|
352
|
+
if (tokensBefore <= targetTokens) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
const pruneResult = pruneToolOutputs(messages, {
|
|
356
|
+
protectTokens: this.config.pruneProtectTokens,
|
|
357
|
+
minimumSavings: this.config.pruneMinimumSavings,
|
|
358
|
+
protectedTools: this.config.pruneProtectedTools,
|
|
359
|
+
provider,
|
|
360
|
+
});
|
|
361
|
+
const tokensAfter = pruneResult.pruned
|
|
362
|
+
? estimateMessagesTokens(pruneResult.messages, provider)
|
|
363
|
+
: tokensBefore;
|
|
364
|
+
logger.info("[Compaction] Stage 1 (prune)", {
|
|
365
|
+
requestId,
|
|
366
|
+
ran: pruneResult.pruned,
|
|
367
|
+
tokensBefore,
|
|
368
|
+
tokensAfter,
|
|
369
|
+
saved: tokensBefore - tokensAfter,
|
|
370
|
+
});
|
|
371
|
+
return pruneResult.pruned ? pruneResult.messages : null;
|
|
372
|
+
}
|
|
257
373
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relevance-driven context compaction.
|
|
3
|
+
*
|
|
4
|
+
* Every stage of the existing compactor is *positional*: prune protects the
|
|
5
|
+
* most recent 40K tokens, truncation drops the oldest half, summarization
|
|
6
|
+
* keeps a trailing ratio. None of them has any notion of what the current
|
|
7
|
+
* request is about, so a fifty-turn conversation that ends with "now rename
|
|
8
|
+
* that variable" keeps the forty turns about database migrations and drops
|
|
9
|
+
* nothing that matters less.
|
|
10
|
+
*
|
|
11
|
+
* A decision model changes the economics of asking. Latency is flat in
|
|
12
|
+
* question count — one question and four hundred questions cost the same
|
|
13
|
+
* round trip — so "is this message needed to answer the current request?" can
|
|
14
|
+
* be asked about *every* message at once, for about $0.00002.
|
|
15
|
+
*
|
|
16
|
+
* What it must never do is lose something the turn needed. The guards below
|
|
17
|
+
* are therefore deliberately conservative, and every one of them is a veto:
|
|
18
|
+
*
|
|
19
|
+
* - only plain user/assistant text is ever eligible; tool calls, tool
|
|
20
|
+
* results, summaries, pinned skills and system messages are untouchable,
|
|
21
|
+
* because dropping half of a tool-call pair produces a malformed request
|
|
22
|
+
* rather than a smaller one
|
|
23
|
+
* - the most recent messages are never eligible, whatever the model says
|
|
24
|
+
* - a message is dropped only on a CONFIDENT no, never on silence or a
|
|
25
|
+
* near-coin-flip
|
|
26
|
+
* - at most a bounded fraction of eligible messages may go in one pass
|
|
27
|
+
*
|
|
28
|
+
* @module context/contextDecision
|
|
29
|
+
*/
|
|
30
|
+
import type { ChatMessage, ContextRelevanceOptions, ContextRelevanceResult, DecisionCallerFn } from "../types/index.js";
|
|
31
|
+
/**
|
|
32
|
+
* Ask which earlier messages the current request still needs, and return the
|
|
33
|
+
* ones to drop.
|
|
34
|
+
*
|
|
35
|
+
* Returns `null` when nothing should change — no decision provider, a failed
|
|
36
|
+
* call, nothing eligible, or no confident drop. A null return is the signal
|
|
37
|
+
* to carry on with positional compaction exactly as before.
|
|
38
|
+
*/
|
|
39
|
+
export declare function selectIrrelevantMessages(messages: ChatMessage[], currentRequest: string, decide: DecisionCallerFn, options?: ContextRelevanceOptions): Promise<ContextRelevanceResult | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Gate a generated summary before it replaces the messages it covers.
|
|
42
|
+
*
|
|
43
|
+
* Stage 3 accepts any non-empty string today, so a summarizer that returned
|
|
44
|
+
* an apology, a refusal, or a truncated fragment silently destroys the
|
|
45
|
+
* conversation it was meant to preserve — and the messages are gone by the
|
|
46
|
+
* time anyone reads the summary.
|
|
47
|
+
*
|
|
48
|
+
* Returns `true` when the summary may be used and `false` only on a confident
|
|
49
|
+
* rejection. Anything else — no decision provider, a failed call, an
|
|
50
|
+
* unanswered or uncertain verdict — returns `true`, because refusing a
|
|
51
|
+
* summary means falling through to truncation, which loses strictly more.
|
|
52
|
+
*/
|
|
53
|
+
export declare function summaryPreservesContext(summary: string, replacedMessages: ChatMessage[], decide: DecisionCallerFn, options?: {
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
minConfidence?: number;
|
|
56
|
+
}): Promise<boolean>;
|