tool-prune 0.1.0 → 0.3.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 CHANGED
@@ -1,9 +1,15 @@
1
1
  # tool-prune
2
2
 
3
- Calibrated tool selection and schema pruning for AI agents. Zero dependencies.
3
+ Calibrated tool selection and schema pruning for AI agents. Dual-engine: zero-dependency offline TurboQuant or TypeSafe System One.
4
4
 
5
5
  ```bash
6
6
  npm install tool-prune
7
+
8
+ # Optional SIMD acceleration
9
+ npm install turboquant-search
10
+
11
+ # Optional: TypeSafe API key for cloud reasoning
12
+ export TYPESAFE_API_KEY="apikey_..."
7
13
  ```
8
14
 
9
15
  ## Quick start
@@ -17,18 +23,21 @@ const tools = {
17
23
  webSearch: 'Search public web for documentation or articles'
18
24
  };
19
25
 
26
+ // Works offline out of the box with TurboQuant:
20
27
  const match = await prune('what tables exist in the db?', tools);
21
- console.log(match.tool); // 'runQuery'
22
- console.log(match.confidence); // 1.0
28
+ console.log(match.tool); // 'runQuery'
29
+ console.log(match.engine); // 'turboquant'
23
30
  ```
24
31
 
25
- `prune()` narrows schemas to Top-K candidates or identifies direct matches in ~140ms. `dispatch()` runs immediate tool execution if confidence clears the threshold. That's the whole API.
32
+ `prune()` narrows schemas offline via TurboQuant by default, or routes to TypeSafe System One when `TYPESAFE_API_KEY` is present. That's the whole API.
26
33
 
27
34
  ## Schema pruning for LLMs
28
35
 
29
36
  ```js
30
37
  const router = prune(tools);
31
- const topTools = await router.filter(userPrompt, { k: 3 });
38
+
39
+ // Auto-selects candidate schemas dynamically (or pass { k: 5 }):
40
+ const topTools = await router.filter(userPrompt);
32
41
 
33
42
  const response = await llm.chat({
34
43
  tools: topTools,
@@ -36,7 +45,7 @@ const response = await llm.chat({
36
45
  });
37
46
  ```
38
47
 
39
- Drops prompt tokens by 75-85% and eliminates context distraction without losing tools.
48
+ Cuts prompt tokens by up to 92% and eliminates context confusion without losing tools.
40
49
 
41
50
  ## Fast-path direct dispatch
42
51
 
@@ -50,14 +59,22 @@ const result = await router.dispatch('read ./package.json', {
50
59
 
51
60
  Runs deterministic handlers in under 160ms with zero token cost.
52
61
 
62
+ ## Dual engine
63
+
64
+ ```js
65
+ const localMatch = await prune(query, tools, { engine: 'turboquant' });
66
+ const cloudMatch = await prune(query, tools, { engine: 'typesafe', apiKey: '...' });
67
+ ```
68
+
69
+ - **turboquant**: 100% offline, zero network, zero dependencies. Uses `turboquant-search` (WASM SIMD) if installed, with built-in FWHT fallback.
70
+ - **typesafe**: Cloud System One reasoning (Jev). 100% Top-1 accuracy on subtle distractors with calibrated probabilities.
71
+
53
72
  ## Demo
54
73
 
55
74
  ```bash
56
75
  npm run demo
57
76
  ```
58
77
 
59
- Runs live quickstart routing with your `TYPESAFE_API_KEY`.
60
-
61
78
  ## License
62
79
 
63
80
  MIT © [Hemanth.HM](https://h3manth.com)
package/index.d.ts CHANGED
@@ -13,13 +13,25 @@ export interface ToolPruneOptions {
13
13
  apiKey?: string;
14
14
  endpoint?: string;
15
15
  model?: string;
16
+ engine?: 'typesafe' | 'turboquant';
16
17
  threshold?: number;
17
- topK?: number;
18
+ topK?: number | 'auto';
19
+ k?: number | 'auto';
20
+ auto?: boolean;
21
+ minK?: number;
22
+ maxK?: number;
23
+ minScore?: number;
24
+ minProbability?: number;
25
+ relativeThreshold?: number;
26
+ cliffRatio?: number;
27
+ dominantMargin?: number;
28
+ allowEmpty?: boolean;
18
29
  }
19
30
 
20
31
  export interface CandidateTool {
21
32
  name: string;
22
33
  probability: number;
34
+ score?: number;
23
35
  tool?: any;
24
36
  }
25
37
 
@@ -28,8 +40,11 @@ export interface SelectionResult {
28
40
  confidence: number;
29
41
  probability: number;
30
42
  topK: CandidateTool[];
43
+ autoSelected: CandidateTool[];
44
+ autoTools: any[];
31
45
  requiresGeneration: number;
32
46
  latency: number;
47
+ engine?: string;
33
48
  usage?: {
34
49
  input_tokens: number;
35
50
  output_tokens: number;
@@ -37,10 +52,13 @@ export interface SelectionResult {
37
52
  raw?: any;
38
53
  }
39
54
 
55
+ export function autoSelectCandidates(candidates: CandidateTool[], options?: Partial<ToolPruneOptions>): CandidateTool[];
56
+
40
57
  export class ToolPruner {
41
58
  constructor(tools: ToolInput, options?: ToolPruneOptions);
42
59
  select(query: string | Record<string, any>, options?: Partial<ToolPruneOptions>): Promise<SelectionResult>;
43
- filter(query: string | Record<string, any>, options?: { k?: number } & Partial<ToolPruneOptions>): Promise<any[]>;
60
+ filter(query: string | Record<string, any>, options?: Partial<ToolPruneOptions>): Promise<any[]>;
61
+ auto(query: string | Record<string, any>, options?: Partial<ToolPruneOptions>): Promise<any[]>;
44
62
  dispatch<T = any>(
45
63
  query: string | Record<string, any>,
46
64
  handlers: Record<string, (query: any, selection: SelectionResult) => Promise<T> | T>,
@@ -51,3 +69,4 @@ export class ToolPruner {
51
69
  export default function toolPrune(query: string, tools: ToolInput, options?: ToolPruneOptions): Promise<SelectionResult>;
52
70
  export default function toolPrune(tools: ToolInput, options?: ToolPruneOptions): ToolPruner;
53
71
 
72
+
package/lib/router.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { TurboQuantEngine } from './turboquant.js';
2
+
1
3
  /**
2
4
  * Normalize diverse tool definitions (Dict, Array, MCP Schema) into a clean criteria map.
3
5
  */
@@ -39,15 +41,36 @@ export class ToolPruner {
39
41
  this.endpoint = options.endpoint || 'https://api.typesafe.ai/v1/systemone';
40
42
  this.model = options.model || 'jev-latest';
41
43
  this.threshold = options.threshold ?? 0.85;
42
- this.defaultTopK = options.topK ?? 3;
44
+ this.defaultTopK = options.topK ?? 'auto';
45
+ this.requestedEngine = options.engine;
46
+ this._tqEngine = null;
47
+ this._wasmEngine = null;
48
+ }
49
+
50
+ /**
51
+ * Determine engine: explicit option, or fallback to turboquant if no API key.
52
+ */
53
+ getEngine(options = {}) {
54
+ const eng = options.engine || this.requestedEngine;
55
+ if (eng) return eng;
56
+ return this.apiKey ? 'typesafe' : 'turboquant';
43
57
  }
44
58
 
45
59
  /**
46
60
  * Select the most appropriate tool for a given query with calibrated confidence.
47
61
  */
48
62
  async select(query, options = {}) {
49
- if (!this.apiKey) {
50
- throw new Error('TypeSafe API key required. Set TYPESAFE_API_KEY or pass { apiKey }.');
63
+ const engine = this.getEngine(options);
64
+ if (engine === 'turboquant') {
65
+ return this._selectTurboQuant(query, options);
66
+ }
67
+ return this._selectTypeSafe(query, options);
68
+ }
69
+
70
+ async _selectTypeSafe(query, options = {}) {
71
+ const key = options.apiKey || this.apiKey;
72
+ if (!key) {
73
+ throw new Error("TypeSafe API key required. Set TYPESAFE_API_KEY, pass { apiKey }, or use { engine: 'turboquant' }.");
51
74
  }
52
75
 
53
76
  const state = typeof query === 'string' ? { intent: query } : query;
@@ -71,7 +94,7 @@ export class ToolPruner {
71
94
  const res = await fetch(this.endpoint, {
72
95
  method: 'POST',
73
96
  headers: {
74
- Authorization: `Bearer ${this.apiKey}`,
97
+ Authorization: `Bearer ${key}`,
75
98
  'Content-Type': 'application/json'
76
99
  },
77
100
  body: JSON.stringify(body)
@@ -88,14 +111,19 @@ export class ToolPruner {
88
111
  const toolAnswer = data.answers?.tool;
89
112
  const probs = toolAnswer?.probabilities || {};
90
113
  const sorted = Object.entries(probs).sort((a, b) => b[1] - a[1]);
91
- const topKCount = options.topK || this.defaultTopK;
92
- const topK = sorted.slice(0, topKCount).map(([name, p]) => ({
114
+ const allCandidates = sorted.map(([name, p]) => ({
93
115
  name,
94
116
  probability: p,
95
117
  tool: this.registry.get(name)
96
118
  }));
97
119
 
98
- const selectedName = toolAnswer?.choice;
120
+ const autoSelected = autoSelectCandidates(allCandidates, options);
121
+ const autoTools = autoSelected.map(c => c.tool || c.name);
122
+
123
+ const k = typeof options.topK === 'number' ? options.topK : (typeof this.defaultTopK === 'number' ? this.defaultTopK : null);
124
+ const topK = typeof k === 'number' ? allCandidates.slice(0, k) : allCandidates.slice(0, Math.max(3, autoSelected.length));
125
+
126
+ const selectedName = toolAnswer?.choice || topK[0]?.name || '';
99
127
  const probability = probs[selectedName] ?? 0;
100
128
  const confidence = toolAnswer?.confidence ?? probability;
101
129
  const requiresGeneration = data.answers?.requires_generation?.probability ?? 0;
@@ -105,20 +133,107 @@ export class ToolPruner {
105
133
  confidence,
106
134
  probability,
107
135
  topK,
136
+ autoSelected,
137
+ autoTools,
108
138
  requiresGeneration,
109
139
  latency,
140
+ engine: 'typesafe',
110
141
  usage: data.usage,
111
142
  raw: data
112
143
  };
113
144
  }
114
145
 
146
+ async _selectTurboQuant(query, options = {}) {
147
+ const qStr = typeof query === 'string' ? query : (query.intent || query.query || JSON.stringify(query));
148
+ const start = performance.now();
149
+
150
+ // Check if turboquant-search (WASM) is available
151
+ if (!this._tqEngine) {
152
+ try {
153
+ const { TurboSearch } = await import('turboquant-search');
154
+ const data = Array.from(this.registry.values()).map(t => ({
155
+ name: t.name,
156
+ description: t.description || '',
157
+ criteria: t.criteria || ''
158
+ }));
159
+ this._wasmEngine = await TurboSearch.from(data, {
160
+ fields: ['name', 'description', 'criteria'],
161
+ dim: 384,
162
+ bits: 3
163
+ });
164
+ } catch {
165
+ // Fallback to built-in zero-dependency TurboQuant engine
166
+ this._tqEngine = new TurboQuantEngine();
167
+ const toolsMap = {};
168
+ for (const [name, tool] of this.registry.entries()) {
169
+ toolsMap[name] = tool;
170
+ }
171
+ this._tqEngine.fit(toolsMap);
172
+ }
173
+ }
174
+
175
+ const k = typeof options.topK === 'number' ? options.topK : (typeof this.defaultTopK === 'number' ? this.defaultTopK : null);
176
+ const poolSize = Math.max(k || 3, 10, this.registry.size);
177
+ let allCandidates = [];
178
+
179
+ if (this._wasmEngine) {
180
+ const results = await this._wasmEngine.search(qStr, { topK: poolSize });
181
+ allCandidates = results.map(r => ({
182
+ name: r.data?.name || '',
183
+ probability: r.score || 0,
184
+ score: r.score || 0,
185
+ tool: this.registry.get(r.data?.name)
186
+ }));
187
+ } else {
188
+ const rawResults = this._tqEngine.search(qStr, poolSize);
189
+ allCandidates = rawResults.map(r => ({
190
+ name: r.name,
191
+ probability: r.probability,
192
+ score: r.score,
193
+ tool: this.registry.get(r.name)
194
+ }));
195
+ }
196
+
197
+ const latency = performance.now() - start;
198
+ const top1 = allCandidates[0];
199
+ const autoSelected = autoSelectCandidates(allCandidates, options);
200
+ const autoTools = autoSelected.map(c => c.tool || c.name);
201
+ const topK = typeof k === 'number' ? allCandidates.slice(0, k) : allCandidates.slice(0, Math.max(3, autoSelected.length));
202
+
203
+ return {
204
+ tool: top1?.name || '',
205
+ confidence: top1?.probability || 0,
206
+ probability: top1?.probability || 0,
207
+ topK,
208
+ autoSelected,
209
+ autoTools,
210
+ requiresGeneration: 0,
211
+ latency,
212
+ engine: 'turboquant'
213
+ };
214
+ }
215
+
115
216
  /**
116
- * Filter tool collection down to top-K candidates to prune LLM prompt bloat.
217
+ * Filter tool collection down to relevant candidates to prune LLM prompt bloat.
218
+ * If k or topK is a number, returns that fixed number of candidates.
219
+ * Otherwise (default or k='auto'), automatically selects the candidates based on score drop-off.
117
220
  */
118
221
  async filter(query, options = {}) {
222
+ const k = options.k ?? options.topK ?? this.defaultTopK;
223
+ if (typeof k === 'number' && k > 0) {
224
+ const res = await this.select(query, { ...options, topK: k });
225
+ return res.topK.slice(0, k).map(item => item.tool || item.name);
226
+ }
227
+ const res = await this.select(query, options);
228
+ return res.autoTools;
229
+ }
230
+
231
+ /**
232
+ * Automatically select the optimal candidate tools based on score distribution.
233
+ */
234
+ async auto(query, options = {}) {
119
235
  const res = await this.select(query, options);
120
- const k = options.k || options.topK || this.defaultTopK;
121
- return res.topK.slice(0, k).map(item => item.tool || item.name);
236
+ return res.autoTools;
122
237
  }
123
238
 
124
239
  /**
@@ -140,6 +255,61 @@ export class ToolPruner {
140
255
  }
141
256
  }
142
257
 
258
+ /**
259
+ * Automatically select the most relevant tool candidates based on score distribution,
260
+ * cliff / elbow drop-off, and relevance floors.
261
+ */
262
+ export function autoSelectCandidates(candidates, options = {}) {
263
+ if (!candidates || candidates.length === 0) return [];
264
+
265
+ const maxK = options.maxK ?? 5;
266
+ const minK = options.minK ?? (options.allowEmpty ? 0 : 1);
267
+ const minScore = options.minScore ?? 0.12;
268
+ const minProb = options.minProbability ?? options.minProb ?? 0.20;
269
+ const relativeThreshold = options.relativeThreshold ?? 0.70;
270
+ const cliffRatio = options.cliffRatio ?? 0.75;
271
+ const dominantMargin = options.dominantMargin ?? 0.14;
272
+
273
+ const top1 = candidates[0];
274
+ const hasScore = typeof top1.score === 'number';
275
+ const topVal = hasScore ? top1.score : top1.probability;
276
+ const floorVal = hasScore ? minScore : minProb;
277
+
278
+ if (topVal < floorVal) {
279
+ return minK > 0 ? candidates.slice(0, minK) : [];
280
+ }
281
+
282
+ const selected = [top1];
283
+
284
+ for (let i = 1; i < Math.min(candidates.length, maxK); i++) {
285
+ const curr = candidates[i];
286
+ const prev = candidates[i - 1];
287
+
288
+ if (hasScore) {
289
+ if (curr.score < minScore) break;
290
+ // Dominant lead: top1 is strong and clearly ahead
291
+ if (top1.score >= 0.35 && (top1.score - curr.score) > dominantMargin) break;
292
+ // Relative to top1
293
+ if ((curr.score / Math.max(1e-6, top1.score)) < relativeThreshold) break;
294
+ // Cliff drop from previous
295
+ if (prev.score > 0 && (curr.score / prev.score) < cliffRatio) break;
296
+ } else {
297
+ if (curr.probability < minProb) break;
298
+ if (top1.probability >= 0.70 && (top1.probability - curr.probability) > 0.20) break;
299
+ if ((curr.probability / Math.max(1e-6, top1.probability)) < relativeThreshold) break;
300
+ if (prev.probability > 0 && (curr.probability / prev.probability) < cliffRatio) break;
301
+ }
302
+
303
+ selected.push(curr);
304
+ }
305
+
306
+ if (selected.length < minK) {
307
+ return candidates.slice(0, Math.min(candidates.length, minK));
308
+ }
309
+
310
+ return selected;
311
+ }
312
+
143
313
  /**
144
314
  * Main function following Hemanth module style:
145
315
  * - One-shot: await toolPrune(query, tools, options)
@@ -153,6 +323,12 @@ export default function toolPrune(arg1, arg2, options) {
153
323
  return new ToolPruner(arg1, arg2);
154
324
  }
155
325
 
326
+ toolPrune.auto = function(query, tools, options) {
327
+ const pruner = new ToolPruner(tools, options);
328
+ return pruner.auto(query, options);
329
+ };
330
+
156
331
  toolPrune.ToolPruner = ToolPruner;
157
332
  toolPrune.normalizeTools = normalizeTools;
333
+ toolPrune.autoSelectCandidates = autoSelectCandidates;
158
334
 
@@ -0,0 +1,186 @@
1
+ /**
2
+ * TurboQuant Vector Search Engine
3
+ * Data-oblivious quantization via Fast Walsh-Hadamard Transform + PolarQuant + QJL residual.
4
+ * Zero external dependencies. Uses turboquant-search (WASM) if installed.
5
+ */
6
+
7
+ // Fast Walsh-Hadamard Transform in O(d log d)
8
+ function fwht(vec) {
9
+ const d = vec.length;
10
+ let h = 1;
11
+ while (h < d) {
12
+ for (let i = 0; i < d; i += 2 * h) {
13
+ for (let j = i; j < i + h; j++) {
14
+ const x = vec[j];
15
+ const y = vec[j + h];
16
+ vec[j] = x + y;
17
+ vec[j + h] = x - y;
18
+ }
19
+ }
20
+ h *= 2;
21
+ }
22
+ const scale = 1 / Math.sqrt(d);
23
+ for (let i = 0; i < d; i++) vec[i] *= scale;
24
+ return vec;
25
+ }
26
+
27
+ function lcg(seed = 42) {
28
+ let s = seed;
29
+ return () => {
30
+ s = (s * 1664525 + 1013904223) % 4294967296;
31
+ return s / 4294967296;
32
+ };
33
+ }
34
+
35
+ export class TurboQuantEngine {
36
+ constructor(options = {}) {
37
+ this.dim = options.dim || 256;
38
+ this.qjlDim = options.qjlDim || 64;
39
+ const rng = lcg(options.seed || 1337);
40
+
41
+ // Diagonal Rademacher matrix (+1 or -1)
42
+ this.rademacher = new Float64Array(this.dim);
43
+ for (let i = 0; i < this.dim; i++) {
44
+ this.rademacher[i] = rng() < 0.5 ? -1 : 1;
45
+ }
46
+
47
+ // QJL random projection matrix
48
+ this.qjlMatrix = new Array(this.qjlDim);
49
+ for (let i = 0; i < this.qjlDim; i++) {
50
+ const row = new Float64Array(this.dim);
51
+ for (let j = 0; j < this.dim; j++) {
52
+ row[j] = (rng() < 0.5 ? -1 : 1) / Math.sqrt(this.qjlDim);
53
+ }
54
+ this.qjlMatrix[i] = row;
55
+ }
56
+
57
+ this.items = [];
58
+ this.index = [];
59
+ }
60
+
61
+ embedText(text) {
62
+ const vec = new Float64Array(this.dim);
63
+ const clean = text.toLowerCase();
64
+ const tokens = clean.replace(/[^a-z0-9_]/g, ' ').split(/\s+/).filter(t => t.length > 1);
65
+
66
+ for (let i = 0; i < tokens.length; i++) {
67
+ const t = tokens[i];
68
+ this._hashFeature(t, 2.0, vec);
69
+ if (i < tokens.length - 1) {
70
+ this._hashFeature(`${t}_${tokens[i + 1]}`, 1.5, vec);
71
+ }
72
+ if (t.length >= 3) {
73
+ for (let j = 0; j <= t.length - 3; j++) {
74
+ this._hashFeature(t.slice(j, j + 3), 0.5, vec);
75
+ }
76
+ }
77
+ }
78
+
79
+ let norm = 0;
80
+ for (let i = 0; i < this.dim; i++) norm += vec[i] * vec[i];
81
+ if (norm > 0) {
82
+ norm = Math.sqrt(norm);
83
+ for (let i = 0; i < this.dim; i++) vec[i] /= norm;
84
+ }
85
+ return vec;
86
+ }
87
+
88
+ _hashFeature(feat, weight, vec) {
89
+ let h = 0x811c9dc5;
90
+ for (let i = 0; i < feat.length; i++) {
91
+ h ^= feat.charCodeAt(i);
92
+ h = Math.imul(h, 0x01000193);
93
+ }
94
+ const idx = Math.abs(h) % this.dim;
95
+ const sign = (h & 1) ? 1 : -1;
96
+ vec[idx] += sign * weight;
97
+ }
98
+
99
+ quantize(vec) {
100
+ const rotated = new Float64Array(this.dim);
101
+ for (let i = 0; i < this.dim; i++) {
102
+ rotated[i] = vec[i] * this.rademacher[i];
103
+ }
104
+ fwht(rotated);
105
+
106
+ const std = 1.0 / Math.sqrt(this.dim);
107
+ const qLevels = new Float64Array([-1.18 * std, -0.38 * std, 0.38 * std, 1.18 * std]);
108
+ const dequant = new Float64Array(this.dim);
109
+ const residual = new Float64Array(this.dim);
110
+
111
+ for (let i = 0; i < this.dim; i++) {
112
+ const val = rotated[i];
113
+ let code = 0;
114
+ if (val < -0.67 * std) code = 0;
115
+ else if (val < 0) code = 1;
116
+ else if (val < 0.67 * std) code = 2;
117
+ else code = 3;
118
+
119
+ dequant[i] = qLevels[code];
120
+ residual[i] = val - dequant[i];
121
+ }
122
+
123
+ let resNorm = 0;
124
+ for (let i = 0; i < this.dim; i++) resNorm += residual[i] * residual[i];
125
+ resNorm = Math.sqrt(resNorm);
126
+
127
+ const qjlSigns = new Int8Array(this.qjlDim);
128
+ for (let i = 0; i < this.qjlDim; i++) {
129
+ let dot = 0;
130
+ const row = this.qjlMatrix[i];
131
+ for (let j = 0; j < this.dim; j++) dot += row[j] * residual[j];
132
+ qjlSigns[i] = dot >= 0 ? 1 : -1;
133
+ }
134
+
135
+ return { dequant, resNorm, qjlSigns };
136
+ }
137
+
138
+ fit(toolsMap) {
139
+ this.items = Object.keys(toolsMap);
140
+ this.index = this.items.map(name => {
141
+ const tool = toolsMap[name];
142
+ const text = typeof tool === 'string'
143
+ ? `${name} ${tool}`
144
+ : `${name} ${tool.description || ''} ${tool.criteria || ''} ${Object.keys(tool.parameters?.properties || {}).join(' ')}`;
145
+ const emb = this.embedText(text);
146
+ return {
147
+ name,
148
+ ...this.quantize(emb)
149
+ };
150
+ });
151
+ }
152
+
153
+ search(query, topK = 3) {
154
+ const qEmb = this.embedText(query);
155
+ const qRotated = new Float64Array(this.dim);
156
+ for (let i = 0; i < this.dim; i++) {
157
+ qRotated[i] = qEmb[i] * this.rademacher[i];
158
+ }
159
+ fwht(qRotated);
160
+
161
+ const qQjl = new Float64Array(this.qjlDim);
162
+ for (let i = 0; i < this.qjlDim; i++) {
163
+ let dot = 0;
164
+ const row = this.qjlMatrix[i];
165
+ for (let j = 0; j < this.dim; j++) dot += row[j] * qRotated[j];
166
+ qQjl[i] = dot;
167
+ }
168
+
169
+ const scores = this.index.map(item => {
170
+ let dotBase = 0;
171
+ for (let i = 0; i < this.dim; i++) dotBase += qRotated[i] * item.dequant[i];
172
+
173
+ let qjlCorrection = 0;
174
+ for (let i = 0; i < this.qjlDim; i++) qjlCorrection += qQjl[i] * item.qjlSigns[i];
175
+
176
+ const residualEst = Math.sqrt(Math.PI / 2) * (item.resNorm / Math.sqrt(this.qjlDim)) * qjlCorrection;
177
+ const rawScore = dotBase + residualEst;
178
+ // Map to calibrated [0, 1] probability range
179
+ const prob = 1 / (1 + Math.exp(-Math.max(-10, Math.min(10, rawScore * 5))));
180
+ return { name: item.name, probability: prob, score: rawScore };
181
+ });
182
+
183
+ scores.sort((a, b) => b.score - a.score);
184
+ return scores.slice(0, topK);
185
+ }
186
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tool-prune",
3
- "version": "0.1.0",
4
- "description": "Fast, calibrated tool selection and schema pruning for AI agents using TypeSafe System One.",
3
+ "version": "0.3.0",
4
+ "description": "Fast, calibrated tool selection and schema pruning for AI agents using TypeSafe System One and TurboQuant vector search.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
@@ -20,11 +20,13 @@
20
20
  ],
21
21
  "scripts": {
22
22
  "test": "node --test test/index.test.js",
23
- "demo": "node examples/quickstart.js"
23
+ "demo": "node examples/quickstart.js",
24
+ "playground": "node ../serve-playground.mjs"
24
25
  },
25
26
  "keywords": [
26
27
  "tool-prune",
27
28
  "tool-selection",
29
+ "turboquant",
28
30
  "mcp",
29
31
  "agent",
30
32
  "typesafe",
@@ -38,6 +40,9 @@
38
40
  "engines": {
39
41
  "node": ">=18.0.0"
40
42
  },
43
+ "optionalDependencies": {
44
+ "turboquant-search": "^0.1.1"
45
+ },
41
46
  "repository": {
42
47
  "type": "git",
43
48
  "url": "https://github.com/hemanth/tool-prune"