tool-prune 0.2.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
@@ -35,7 +35,9 @@ console.log(match.engine); // 'turboquant'
35
35
 
36
36
  ```js
37
37
  const router = prune(tools);
38
- const topTools = await router.filter(userPrompt, { k: 5 });
38
+
39
+ // Auto-selects candidate schemas dynamically (or pass { k: 5 }):
40
+ const topTools = await router.filter(userPrompt);
39
41
 
40
42
  const response = await llm.chat({
41
43
  tools: topTools,
package/index.d.ts CHANGED
@@ -15,7 +15,17 @@ export interface ToolPruneOptions {
15
15
  model?: string;
16
16
  engine?: 'typesafe' | 'turboquant';
17
17
  threshold?: number;
18
- 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;
19
29
  }
20
30
 
21
31
  export interface CandidateTool {
@@ -30,6 +40,8 @@ export interface SelectionResult {
30
40
  confidence: number;
31
41
  probability: number;
32
42
  topK: CandidateTool[];
43
+ autoSelected: CandidateTool[];
44
+ autoTools: any[];
33
45
  requiresGeneration: number;
34
46
  latency: number;
35
47
  engine?: string;
@@ -40,10 +52,13 @@ export interface SelectionResult {
40
52
  raw?: any;
41
53
  }
42
54
 
55
+ export function autoSelectCandidates(candidates: CandidateTool[], options?: Partial<ToolPruneOptions>): CandidateTool[];
56
+
43
57
  export class ToolPruner {
44
58
  constructor(tools: ToolInput, options?: ToolPruneOptions);
45
59
  select(query: string | Record<string, any>, options?: Partial<ToolPruneOptions>): Promise<SelectionResult>;
46
- 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[]>;
47
62
  dispatch<T = any>(
48
63
  query: string | Record<string, any>,
49
64
  handlers: Record<string, (query: any, selection: SelectionResult) => Promise<T> | T>,
@@ -54,3 +69,4 @@ export class ToolPruner {
54
69
  export default function toolPrune(query: string, tools: ToolInput, options?: ToolPruneOptions): Promise<SelectionResult>;
55
70
  export default function toolPrune(tools: ToolInput, options?: ToolPruneOptions): ToolPruner;
56
71
 
72
+
package/lib/router.js CHANGED
@@ -41,7 +41,7 @@ export class ToolPruner {
41
41
  this.endpoint = options.endpoint || 'https://api.typesafe.ai/v1/systemone';
42
42
  this.model = options.model || 'jev-latest';
43
43
  this.threshold = options.threshold ?? 0.85;
44
- this.defaultTopK = options.topK ?? 3;
44
+ this.defaultTopK = options.topK ?? 'auto';
45
45
  this.requestedEngine = options.engine;
46
46
  this._tqEngine = null;
47
47
  this._wasmEngine = null;
@@ -111,14 +111,19 @@ export class ToolPruner {
111
111
  const toolAnswer = data.answers?.tool;
112
112
  const probs = toolAnswer?.probabilities || {};
113
113
  const sorted = Object.entries(probs).sort((a, b) => b[1] - a[1]);
114
- const topKCount = options.topK || this.defaultTopK;
115
- const topK = sorted.slice(0, topKCount).map(([name, p]) => ({
114
+ const allCandidates = sorted.map(([name, p]) => ({
116
115
  name,
117
116
  probability: p,
118
117
  tool: this.registry.get(name)
119
118
  }));
120
119
 
121
- 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 || '';
122
127
  const probability = probs[selectedName] ?? 0;
123
128
  const confidence = toolAnswer?.confidence ?? probability;
124
129
  const requiresGeneration = data.answers?.requires_generation?.probability ?? 0;
@@ -128,6 +133,8 @@ export class ToolPruner {
128
133
  confidence,
129
134
  probability,
130
135
  topK,
136
+ autoSelected,
137
+ autoTools,
131
138
  requiresGeneration,
132
139
  latency,
133
140
  engine: 'typesafe',
@@ -165,20 +172,21 @@ export class ToolPruner {
165
172
  }
166
173
  }
167
174
 
168
- const k = options.topK || this.defaultTopK;
169
- let topK = [];
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 = [];
170
178
 
171
179
  if (this._wasmEngine) {
172
- const results = await this._wasmEngine.search(qStr, { topK: k });
173
- topK = results.map(r => ({
180
+ const results = await this._wasmEngine.search(qStr, { topK: poolSize });
181
+ allCandidates = results.map(r => ({
174
182
  name: r.data?.name || '',
175
183
  probability: r.score || 0,
176
184
  score: r.score || 0,
177
185
  tool: this.registry.get(r.data?.name)
178
186
  }));
179
187
  } else {
180
- const rawResults = this._tqEngine.search(qStr, k);
181
- topK = rawResults.map(r => ({
188
+ const rawResults = this._tqEngine.search(qStr, poolSize);
189
+ allCandidates = rawResults.map(r => ({
182
190
  name: r.name,
183
191
  probability: r.probability,
184
192
  score: r.score,
@@ -187,13 +195,18 @@ export class ToolPruner {
187
195
  }
188
196
 
189
197
  const latency = performance.now() - start;
190
- const top1 = topK[0];
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));
191
202
 
192
203
  return {
193
204
  tool: top1?.name || '',
194
205
  confidence: top1?.probability || 0,
195
206
  probability: top1?.probability || 0,
196
207
  topK,
208
+ autoSelected,
209
+ autoTools,
197
210
  requiresGeneration: 0,
198
211
  latency,
199
212
  engine: 'turboquant'
@@ -201,12 +214,26 @@ export class ToolPruner {
201
214
  }
202
215
 
203
216
  /**
204
- * 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.
205
220
  */
206
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 = {}) {
207
235
  const res = await this.select(query, options);
208
- const k = options.k || options.topK || this.defaultTopK;
209
- return res.topK.slice(0, k).map(item => item.tool || item.name);
236
+ return res.autoTools;
210
237
  }
211
238
 
212
239
  /**
@@ -228,6 +255,61 @@ export class ToolPruner {
228
255
  }
229
256
  }
230
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
+
231
313
  /**
232
314
  * Main function following Hemanth module style:
233
315
  * - One-shot: await toolPrune(query, tools, options)
@@ -241,5 +323,12 @@ export default function toolPrune(arg1, arg2, options) {
241
323
  return new ToolPruner(arg1, arg2);
242
324
  }
243
325
 
326
+ toolPrune.auto = function(query, tools, options) {
327
+ const pruner = new ToolPruner(tools, options);
328
+ return pruner.auto(query, options);
329
+ };
330
+
244
331
  toolPrune.ToolPruner = ToolPruner;
245
332
  toolPrune.normalizeTools = normalizeTools;
333
+ toolPrune.autoSelectCandidates = autoSelectCandidates;
334
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tool-prune",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
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",
@@ -20,7 +20,8 @@
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",