efaimo 0.1.2 → 0.2.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 +63 -1
- package/README.md +68 -6
- package/SKILL.md +14 -7
- package/dist/cli.js +162 -22
- package/dist/core/engine.d.ts +2 -1
- package/dist/core/engine.js +4 -0
- package/dist/core/types.d.ts +121 -0
- package/dist/find/bm25.d.ts +77 -0
- package/dist/find/bm25.js +107 -0
- package/dist/find/find.d.ts +120 -0
- package/dist/find/find.js +403 -0
- package/dist/find/tokenize.d.ts +64 -0
- package/dist/find/tokenize.js +116 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +13 -3
- package/dist/reporters/json.d.ts +19 -1
- package/dist/reporters/json.js +13 -2
- package/dist/reporters/markdown.d.ts +2 -1
- package/dist/reporters/markdown.js +109 -11
- package/dist/reporters/pretty.d.ts +12 -1
- package/dist/reporters/pretty.js +174 -9
- package/dist/rules/find/index.d.ts +4 -0
- package/dist/rules/find/index.js +262 -0
- package/dist/rules/version.d.ts +26 -0
- package/dist/rules/version.js +26 -0
- package/dist/testing/anthropicRunner.d.ts +30 -0
- package/dist/testing/anthropicRunner.js +43 -8
- package/dist/testing/harness.d.ts +32 -1
- package/dist/testing/harness.js +28 -3
- package/dist/testing/stats.d.ts +16 -0
- package/dist/testing/stats.js +26 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/weigh/anthropicExact.d.ts +12 -1
- package/dist/weigh/anthropicExact.js +9 -1
- package/dist/weigh/weigh.js +10 -3
- package/docs/INTEGRATIONS.md +15 -1
- package/docs/METHODOLOGY.md +110 -6
- package/docs/RULES.md +40 -3
- package/examples/scenario.crm.yaml +8 -0
- package/examples/scenario.example.yaml +8 -0
- package/package.json +1 -1
package/dist/core/types.d.ts
CHANGED
|
@@ -155,6 +155,13 @@ export interface ServerWeighResult {
|
|
|
155
155
|
instructionsTokens: number;
|
|
156
156
|
/** Exact count via Anthropic count_tokens API, when a key was provided. */
|
|
157
157
|
anthropicExactTotal?: number;
|
|
158
|
+
/**
|
|
159
|
+
* The model `anthropicExactTotal` was measured against. Always set when the
|
|
160
|
+
* total is: Claude model lines do not share a tokenizer, so the figure is
|
|
161
|
+
* exact for this model and approximate for any other, and a count printed
|
|
162
|
+
* without its model cannot be reproduced.
|
|
163
|
+
*/
|
|
164
|
+
anthropicExactModel?: string;
|
|
158
165
|
notes: string[];
|
|
159
166
|
}
|
|
160
167
|
export interface SkillWeighEntry {
|
|
@@ -178,6 +185,111 @@ export interface SkillSetWeighResult {
|
|
|
178
185
|
notes: string[];
|
|
179
186
|
}
|
|
180
187
|
export type WeighResult = ServerWeighResult | SkillSetWeighResult;
|
|
188
|
+
/** One tool's result in the findability report (`efaimo find`). */
|
|
189
|
+
export interface ToolFindEntry {
|
|
190
|
+
name: string;
|
|
191
|
+
/**
|
|
192
|
+
* Terms this tool has that no other tool in the catalog has.
|
|
193
|
+
*
|
|
194
|
+
* The primary signal, and the only one here that is not a model of anything:
|
|
195
|
+
* if this is empty, every word the tool contains also appears on some other
|
|
196
|
+
* tool, so no query can match it without also matching a competitor. That is
|
|
197
|
+
* a property of the catalog, provable by inspection, with no assumption
|
|
198
|
+
* about how anyone searches. Capped for display.
|
|
199
|
+
*/
|
|
200
|
+
ownTerms: string[];
|
|
201
|
+
/** How many exclusive terms there are in total, before the display cap. */
|
|
202
|
+
ownTermCount: number;
|
|
203
|
+
/**
|
|
204
|
+
* How many of them survive if the searcher does not know the tool's name.
|
|
205
|
+
*
|
|
206
|
+
* Names are searchable, so a tool whose only exclusive word is its own name
|
|
207
|
+
* is findable in principle. It is not findable by anyone describing a task,
|
|
208
|
+
* which is the situation deferred loading creates. E146 is this number
|
|
209
|
+
* reaching zero while `ownTermCount` does not.
|
|
210
|
+
*/
|
|
211
|
+
ownOutsideNameCount: number;
|
|
212
|
+
/** When ownTermCount is 0: tools whose vocabulary covers this one's (capped). */
|
|
213
|
+
sharedWith: string[];
|
|
214
|
+
/**
|
|
215
|
+
* The terms this tool's own description offers a searcher, most distinctive
|
|
216
|
+
* first. Never includes the tool's name: the point of the probe is that the
|
|
217
|
+
* person searching does not know it.
|
|
218
|
+
*/
|
|
219
|
+
query: string[];
|
|
220
|
+
/** 1-based rank for that query. Undefined when the description offers no searchable word. */
|
|
221
|
+
rank?: number;
|
|
222
|
+
/** BM25 score for its own query, kept so a reader can check the arithmetic. */
|
|
223
|
+
score: number;
|
|
224
|
+
/** Tools that scored higher for this tool's own query (capped at 3). */
|
|
225
|
+
outrankedBy: string[];
|
|
226
|
+
/** Tools whose score is indistinguishable from this one's, so only the tie-break separates them. Capped for display. */
|
|
227
|
+
tiedWith: string[];
|
|
228
|
+
/** How many there are in total, before the display cap. */
|
|
229
|
+
tiedWithCount: number;
|
|
230
|
+
/** Rank falls inside the simulated result window. The per-tool form of `probe`. */
|
|
231
|
+
reachable: boolean;
|
|
232
|
+
/** Every token of the name is a generic word (E144). */
|
|
233
|
+
genericName: boolean;
|
|
234
|
+
}
|
|
235
|
+
export interface FindResult {
|
|
236
|
+
kind: "find";
|
|
237
|
+
label: string;
|
|
238
|
+
toolCount: number;
|
|
239
|
+
/** The whole method, carried with the numbers so a report is self-describing. */
|
|
240
|
+
method: {
|
|
241
|
+
tokenizer: string;
|
|
242
|
+
bm25: {
|
|
243
|
+
k1: number;
|
|
244
|
+
b: number;
|
|
245
|
+
};
|
|
246
|
+
queryTerms: number;
|
|
247
|
+
topK: number;
|
|
248
|
+
};
|
|
249
|
+
perTool: ToolFindEntry[];
|
|
250
|
+
/**
|
|
251
|
+
* The headline: how many tools own at least one word no other tool has.
|
|
252
|
+
* A measured proportion, deliberately not a letter grade (ADR-030).
|
|
253
|
+
*/
|
|
254
|
+
distinct: {
|
|
255
|
+
count: number;
|
|
256
|
+
total: number;
|
|
257
|
+
pct: number;
|
|
258
|
+
};
|
|
259
|
+
/**
|
|
260
|
+
* Secondary: how many tools a simulated BM25 search for their own
|
|
261
|
+
* description returns inside the result window.
|
|
262
|
+
*
|
|
263
|
+
* Named `probe` in the JSON, in the output and in the docs, all three. It
|
|
264
|
+
* was `reach` in one of them and `probe` in the others for a day, which is
|
|
265
|
+
* how someone scripting a gate on the number they saw printed ends up
|
|
266
|
+
* looking for a field that does not exist.
|
|
267
|
+
*
|
|
268
|
+
* Kept, and kept second, because it is the only number here that models the
|
|
269
|
+
* actual mechanism, and because it saturates: 100% on every real server
|
|
270
|
+
* measured. Treat it as a floor test that catches catastrophe, not as a
|
|
271
|
+
* ranking. See docs/METHODOLOGY.md.
|
|
272
|
+
*/
|
|
273
|
+
probe: {
|
|
274
|
+
returned: number;
|
|
275
|
+
total: number;
|
|
276
|
+
pct: number;
|
|
277
|
+
};
|
|
278
|
+
/** toolCount <= topK, which makes the probe figure 100% by construction. */
|
|
279
|
+
windowCoversCatalog: boolean;
|
|
280
|
+
/**
|
|
281
|
+
* toolCount < 2, which makes the DISTINCT figure 100% by construction: with
|
|
282
|
+
* one tool every term it has is trivially exclusive. `--min-distinct` cannot
|
|
283
|
+
* be evaluated in that state and refuses rather than passing.
|
|
284
|
+
*/
|
|
285
|
+
distinctVacuous: boolean;
|
|
286
|
+
definitionTokens?: number;
|
|
287
|
+
/** Anthropic's guidance recommends tool search for a catalog this shape. */
|
|
288
|
+
deferRecommended: boolean;
|
|
289
|
+
/** Which of the checkable conditions fired, in words. Empty when none did. */
|
|
290
|
+
deferBecause: string[];
|
|
291
|
+
notes: string[];
|
|
292
|
+
}
|
|
181
293
|
export interface SkillInfo {
|
|
182
294
|
dir: string;
|
|
183
295
|
file: string;
|
|
@@ -248,6 +360,9 @@ export interface SkillRuleContext {
|
|
|
248
360
|
set: SkillSet;
|
|
249
361
|
weigh?: SkillSetWeighResult;
|
|
250
362
|
}
|
|
363
|
+
export interface FindRuleContext {
|
|
364
|
+
find: FindResult;
|
|
365
|
+
}
|
|
251
366
|
export interface McpRule {
|
|
252
367
|
id: string;
|
|
253
368
|
title: string;
|
|
@@ -260,3 +375,9 @@ export interface SkillRule {
|
|
|
260
375
|
surface: "skill";
|
|
261
376
|
check(ctx: SkillRuleContext): Finding[];
|
|
262
377
|
}
|
|
378
|
+
export interface FindRule {
|
|
379
|
+
id: string;
|
|
380
|
+
title: string;
|
|
381
|
+
surface: "find";
|
|
382
|
+
check(ctx: FindRuleContext): Finding[];
|
|
383
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small, exact BM25 index.
|
|
3
|
+
*
|
|
4
|
+
* `efaimo find` needs to answer "would a search over this catalog surface this
|
|
5
|
+
* tool", and the built-in tool search is server-side: its parameters are not
|
|
6
|
+
* published, so nothing here can be a reproduction. It is the textbook
|
|
7
|
+
* Robertson/Sparck-Jones BM25 with its conventional parameters, run offline on
|
|
8
|
+
* the four fields Anthropic documents as searchable, and both the formula and
|
|
9
|
+
* the parameters are printed in the output. A reader can disagree with the
|
|
10
|
+
* model and still check the arithmetic.
|
|
11
|
+
*
|
|
12
|
+
* Two properties this file exists to guarantee:
|
|
13
|
+
*
|
|
14
|
+
* - **Determinism.** Ties break on the document id by code point, never by
|
|
15
|
+
* `localeCompare`, whose collation depends on the host's ICU data. A tool
|
|
16
|
+
* that reports other people's nondeterminism (E112) must not have any of its
|
|
17
|
+
* own, and "it ranked differently on CI" is exactly the bug that would be
|
|
18
|
+
* impossible to reproduce.
|
|
19
|
+
* - **No silent empties.** `score()` on an empty query returns an empty array
|
|
20
|
+
* rather than the whole catalog at score 0. Callers treat that as a finding,
|
|
21
|
+
* not as a pass.
|
|
22
|
+
*/
|
|
23
|
+
export interface Bm25Doc {
|
|
24
|
+
id: string;
|
|
25
|
+
terms: string[];
|
|
26
|
+
}
|
|
27
|
+
export interface Bm25Hit {
|
|
28
|
+
id: string;
|
|
29
|
+
score: number;
|
|
30
|
+
/**
|
|
31
|
+
* The document's position in the corpus.
|
|
32
|
+
*
|
|
33
|
+
* Present because `id` is not a key. MCP does not forbid two tools sharing a
|
|
34
|
+
* name, and a caller that located its own row with `findIndex(h => h.id ===
|
|
35
|
+
* name)` got the better-scoring twin's rank, score and tie set: a tool that
|
|
36
|
+
* really sat eighth reported rank 1, and the headline moved in the flattering
|
|
37
|
+
* direction. Match on this instead.
|
|
38
|
+
*/
|
|
39
|
+
index: number;
|
|
40
|
+
}
|
|
41
|
+
export interface Bm25Params {
|
|
42
|
+
k1: number;
|
|
43
|
+
b: number;
|
|
44
|
+
}
|
|
45
|
+
/** The conventional defaults. Printed in every report so the method travels with the number. */
|
|
46
|
+
export declare const DEFAULT_BM25: Bm25Params;
|
|
47
|
+
export declare class Bm25Index {
|
|
48
|
+
readonly params: Bm25Params;
|
|
49
|
+
readonly size: number;
|
|
50
|
+
private readonly ids;
|
|
51
|
+
private readonly tf;
|
|
52
|
+
private readonly len;
|
|
53
|
+
private readonly df;
|
|
54
|
+
private readonly avgdl;
|
|
55
|
+
constructor(docs: readonly Bm25Doc[], params?: Bm25Params);
|
|
56
|
+
/** BM25 IDF. Always positive, so a term present in every document still contributes. */
|
|
57
|
+
idf(term: string): number;
|
|
58
|
+
/** Document frequency: how many tools contain this term at all. */
|
|
59
|
+
documentFrequency(term: string): number;
|
|
60
|
+
termFrequency(index: number, term: string): number;
|
|
61
|
+
/**
|
|
62
|
+
* Score every document against a query, highest first.
|
|
63
|
+
*
|
|
64
|
+
* Duplicate query terms are collapsed: a query is a set of keywords here,
|
|
65
|
+
* not a passage, and counting a repeated term twice would silently weight it.
|
|
66
|
+
*/
|
|
67
|
+
score(query: readonly string[]): Bm25Hit[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Are two scores the same as far as a ranker is concerned?
|
|
71
|
+
*
|
|
72
|
+
* Relative, because BM25 scores are unbounded: an absolute epsilon that is
|
|
73
|
+
* right at score 2 is meaningless at score 40. Two tools this close are
|
|
74
|
+
* separated only by the tie-break, which means the search picks between them
|
|
75
|
+
* on the alphabet rather than on relevance.
|
|
76
|
+
*/
|
|
77
|
+
export declare function scoresTie(a: number, bScore: number, rel?: number): boolean;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small, exact BM25 index.
|
|
3
|
+
*
|
|
4
|
+
* `efaimo find` needs to answer "would a search over this catalog surface this
|
|
5
|
+
* tool", and the built-in tool search is server-side: its parameters are not
|
|
6
|
+
* published, so nothing here can be a reproduction. It is the textbook
|
|
7
|
+
* Robertson/Sparck-Jones BM25 with its conventional parameters, run offline on
|
|
8
|
+
* the four fields Anthropic documents as searchable, and both the formula and
|
|
9
|
+
* the parameters are printed in the output. A reader can disagree with the
|
|
10
|
+
* model and still check the arithmetic.
|
|
11
|
+
*
|
|
12
|
+
* Two properties this file exists to guarantee:
|
|
13
|
+
*
|
|
14
|
+
* - **Determinism.** Ties break on the document id by code point, never by
|
|
15
|
+
* `localeCompare`, whose collation depends on the host's ICU data. A tool
|
|
16
|
+
* that reports other people's nondeterminism (E112) must not have any of its
|
|
17
|
+
* own, and "it ranked differently on CI" is exactly the bug that would be
|
|
18
|
+
* impossible to reproduce.
|
|
19
|
+
* - **No silent empties.** `score()` on an empty query returns an empty array
|
|
20
|
+
* rather than the whole catalog at score 0. Callers treat that as a finding,
|
|
21
|
+
* not as a pass.
|
|
22
|
+
*/
|
|
23
|
+
/** The conventional defaults. Printed in every report so the method travels with the number. */
|
|
24
|
+
export const DEFAULT_BM25 = { k1: 1.2, b: 0.75 };
|
|
25
|
+
export class Bm25Index {
|
|
26
|
+
params;
|
|
27
|
+
size;
|
|
28
|
+
ids;
|
|
29
|
+
tf;
|
|
30
|
+
len;
|
|
31
|
+
df = new Map();
|
|
32
|
+
avgdl;
|
|
33
|
+
constructor(docs, params = DEFAULT_BM25) {
|
|
34
|
+
this.params = params;
|
|
35
|
+
this.size = docs.length;
|
|
36
|
+
this.ids = docs.map((d) => d.id);
|
|
37
|
+
this.tf = docs.map((d) => {
|
|
38
|
+
const m = new Map();
|
|
39
|
+
for (const t of d.terms)
|
|
40
|
+
m.set(t, (m.get(t) ?? 0) + 1);
|
|
41
|
+
return m;
|
|
42
|
+
});
|
|
43
|
+
this.len = docs.map((d) => d.terms.length);
|
|
44
|
+
for (const m of this.tf) {
|
|
45
|
+
for (const t of m.keys())
|
|
46
|
+
this.df.set(t, (this.df.get(t) ?? 0) + 1);
|
|
47
|
+
}
|
|
48
|
+
const total = this.len.reduce((a, b) => a + b, 0);
|
|
49
|
+
// A catalog of zero-length documents would divide by zero in the length
|
|
50
|
+
// normalization. It cannot happen with real tools (a name always tokenizes
|
|
51
|
+
// to something) but the guard costs nothing and a NaN score would rank
|
|
52
|
+
// unpredictably rather than fail.
|
|
53
|
+
this.avgdl = docs.length ? Math.max(1, total / docs.length) : 1;
|
|
54
|
+
}
|
|
55
|
+
/** BM25 IDF. Always positive, so a term present in every document still contributes. */
|
|
56
|
+
idf(term) {
|
|
57
|
+
const n = this.df.get(term) ?? 0;
|
|
58
|
+
return Math.log(1 + (this.size - n + 0.5) / (n + 0.5));
|
|
59
|
+
}
|
|
60
|
+
/** Document frequency: how many tools contain this term at all. */
|
|
61
|
+
documentFrequency(term) {
|
|
62
|
+
return this.df.get(term) ?? 0;
|
|
63
|
+
}
|
|
64
|
+
termFrequency(index, term) {
|
|
65
|
+
return this.tf[index]?.get(term) ?? 0;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Score every document against a query, highest first.
|
|
69
|
+
*
|
|
70
|
+
* Duplicate query terms are collapsed: a query is a set of keywords here,
|
|
71
|
+
* not a passage, and counting a repeated term twice would silently weight it.
|
|
72
|
+
*/
|
|
73
|
+
score(query) {
|
|
74
|
+
const terms = [...new Set(query)];
|
|
75
|
+
if (!terms.length)
|
|
76
|
+
return [];
|
|
77
|
+
const { k1, b } = this.params;
|
|
78
|
+
const hits = [];
|
|
79
|
+
for (let i = 0; i < this.size; i++) {
|
|
80
|
+
let s = 0;
|
|
81
|
+
const norm = k1 * (1 - b + (b * this.len[i]) / this.avgdl);
|
|
82
|
+
for (const t of terms) {
|
|
83
|
+
const f = this.tf[i].get(t);
|
|
84
|
+
if (!f)
|
|
85
|
+
continue;
|
|
86
|
+
s += this.idf(t) * ((f * (k1 + 1)) / (f + norm));
|
|
87
|
+
}
|
|
88
|
+
hits.push({ id: this.ids[i], score: s, index: i });
|
|
89
|
+
}
|
|
90
|
+
// Ties break on the id by code point, then on position, so two documents
|
|
91
|
+
// with the same name and the same score still have a fixed order.
|
|
92
|
+
hits.sort((x, y) => (y.score - x.score) || (x.id < y.id ? -1 : x.id > y.id ? 1 : 0) || (x.index - y.index));
|
|
93
|
+
return hits;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Are two scores the same as far as a ranker is concerned?
|
|
98
|
+
*
|
|
99
|
+
* Relative, because BM25 scores are unbounded: an absolute epsilon that is
|
|
100
|
+
* right at score 2 is meaningless at score 40. Two tools this close are
|
|
101
|
+
* separated only by the tie-break, which means the search picks between them
|
|
102
|
+
* on the alphabet rather than on relevance.
|
|
103
|
+
*/
|
|
104
|
+
export function scoresTie(a, bScore, rel = 1e-9) {
|
|
105
|
+
const scale = Math.max(Math.abs(a), Math.abs(bScore), 1);
|
|
106
|
+
return Math.abs(a - bScore) <= rel * scale;
|
|
107
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { FindResult, ToolDef } from "../core/types.js";
|
|
2
|
+
import { Bm25Index } from "./bm25.js";
|
|
3
|
+
/**
|
|
4
|
+
* Findability: can a search over this catalog pick this tool out at all?
|
|
5
|
+
*
|
|
6
|
+
* Why this exists. Since 2026-01 a host can mark tools `defer_loading: true`,
|
|
7
|
+
* which keeps them out of the system-prompt prefix entirely; the model then
|
|
8
|
+
* finds them by searching the catalog, and Anthropic recommends turning that
|
|
9
|
+
* on once tool definitions pass ~10k tokens. Under deferral a tool no search
|
|
10
|
+
* surfaces costs zero tokens and provides zero capability, and no existing
|
|
11
|
+
* check looks at that: `weigh` measures what a tool costs, `check` measures
|
|
12
|
+
* what a model experiences once the tool is already in front of it.
|
|
13
|
+
*
|
|
14
|
+
* Two measurements, and they are not the same kind of thing.
|
|
15
|
+
*
|
|
16
|
+
* 1. EXCLUSIVE VOCABULARY, the headline. Which terms does a tool have that no
|
|
17
|
+
* other tool in the catalog has? A tool with none cannot be matched by any
|
|
18
|
+
* query that does not also match a competitor, because every word it
|
|
19
|
+
* contains is somewhere else too. That is a fact about the catalog, checked
|
|
20
|
+
* by inspection, with no model of how anyone searches, no parameters, and
|
|
21
|
+
* nothing to disagree with except the tokenizer.
|
|
22
|
+
*
|
|
23
|
+
* It also discriminates, which the alternative did not. Measured on
|
|
24
|
+
* `@playwright/mcp` 0.0.78: 22 of 24 tools own a word, and `browser_close`
|
|
25
|
+
* and `browser_navigate` own none.
|
|
26
|
+
*
|
|
27
|
+
* 2. THE SEARCH PROBE, secondary. Simulated BM25 over the four fields tool
|
|
28
|
+
* search documents as searchable, each tool queried with the top terms of
|
|
29
|
+
* its own description. This is a model, and a generous one: the query comes
|
|
30
|
+
* from the tool's own words. It reads 100% on both the official reference
|
|
31
|
+
* server and playwright, so it is a floor test that catches catastrophe -
|
|
32
|
+
* an empty description, a literal duplicate - and not a ranking. The report
|
|
33
|
+
* says so on every run rather than letting a saturated number look like a
|
|
34
|
+
* good score.
|
|
35
|
+
*
|
|
36
|
+
* Neither is a grade. See ADR-030 for why this ships as proportions.
|
|
37
|
+
*/
|
|
38
|
+
/** How many of a tool's own terms make up its self-query. */
|
|
39
|
+
export declare const DEFAULT_QUERY_TERMS = 4;
|
|
40
|
+
/**
|
|
41
|
+
* How many results a search returns. Anthropic documents a default of 5 (the
|
|
42
|
+
* model may raise it), so 5 is what a tool has to land inside to be seen
|
|
43
|
+
* without the model deciding to look harder.
|
|
44
|
+
*/
|
|
45
|
+
export declare const DEFAULT_TOP_K = 5;
|
|
46
|
+
/**
|
|
47
|
+
* When Anthropic's own guidance says to turn tool search on.
|
|
48
|
+
*
|
|
49
|
+
* The doc lists five conditions joined by "any of the following apply", and
|
|
50
|
+
* two of them can be checked from a tool list: "You have 10 or more tools
|
|
51
|
+
* available" and "Your tool definitions consume more than 10k tokens". The
|
|
52
|
+
* other three are about the operator's situation, not the catalog.
|
|
53
|
+
*
|
|
54
|
+
* Both are checked. An earlier version implemented only the token clause,
|
|
55
|
+
* which told a reader that a 24-tool, 3.5k-token server was "probably loaded
|
|
56
|
+
* up front today" when Anthropic's first condition already applied to it, and
|
|
57
|
+
* left E145 structurally unable to fire on the most common shape there is: a
|
|
58
|
+
* catalog with plenty of tools and modest descriptions.
|
|
59
|
+
*/
|
|
60
|
+
export declare const DEFER_RECOMMENDED_TOKENS = 10000;
|
|
61
|
+
export declare const DEFER_RECOMMENDED_TOOLS = 10;
|
|
62
|
+
/**
|
|
63
|
+
* The four fields tool search matches on: name, description, argument names,
|
|
64
|
+
* argument descriptions. Kept in one function so the index and the docs can
|
|
65
|
+
* never disagree about what is searched.
|
|
66
|
+
*
|
|
67
|
+
* `title` is deliberately NOT among them, for two reasons that point the same
|
|
68
|
+
* way. Anthropic's list of searchable fields is these four and does not
|
|
69
|
+
* include it. And MCP `title` is a human-readable restatement of the name:
|
|
70
|
+
* every one of the reference server's thirteen tools sets one, and every one
|
|
71
|
+
* is the name title-cased ("Echo Tool", "Get Annotated Message Tool"). An
|
|
72
|
+
* earlier version indexed and PROBED with it, which quietly put the tool's own
|
|
73
|
+
* name back into its own query through the back door and undid the whole
|
|
74
|
+
* reason the name is excluded. Two tools with identical descriptions and
|
|
75
|
+
* name-derived titles scored a clean rank 1 each instead of tying.
|
|
76
|
+
*
|
|
77
|
+
* Indexing a field the real search does not read would also make `distinct`
|
|
78
|
+
* optimistic: a tool could "own" a word that exists only in its title, where
|
|
79
|
+
* no query can reach it.
|
|
80
|
+
*/
|
|
81
|
+
export declare function indexedTerms(tool: ToolDef): string[];
|
|
82
|
+
/**
|
|
83
|
+
* The terms a person would plausibly type to reach this tool.
|
|
84
|
+
*
|
|
85
|
+
* Drawn from the description, and deliberately NOT from the name, the title,
|
|
86
|
+
* or the argument names.
|
|
87
|
+
*
|
|
88
|
+
* Not the name, because the person searching does not know it. That is the
|
|
89
|
+
* whole situation being modelled: the tool is deferred, the user describes a
|
|
90
|
+
* task, and the model turns that description into a query. An earlier version
|
|
91
|
+
* of this drew query terms from the name too, and the result was a metric that
|
|
92
|
+
* could barely fail - a unique name token has maximal idf, so it entered every
|
|
93
|
+
* query and pulled its own tool to the top. Two tools with the same
|
|
94
|
+
* description and different names both scored a clean rank 1, which is exactly
|
|
95
|
+
* backwards: identical descriptions are the case where a searcher cannot tell
|
|
96
|
+
* them apart. The name stays in the INDEX, so a name whose words echo the
|
|
97
|
+
* description still helps the tool rank.
|
|
98
|
+
*
|
|
99
|
+
* Not the title either, and that one is the same bug wearing a different hat:
|
|
100
|
+
* MCP `title` is usually the name title-cased, so reading it here put the name
|
|
101
|
+
* back into its own query after all the trouble taken to keep it out. See
|
|
102
|
+
* `indexedTerms`, which drops it from the index for the same reason.
|
|
103
|
+
*
|
|
104
|
+
* Not argument names, because a user describes a task ("post a message to
|
|
105
|
+
* slack"), not a parameter list. They are indexed, so a search can still reach
|
|
106
|
+
* them; they are just never what the tool is probed with.
|
|
107
|
+
*
|
|
108
|
+
* Ranked by tf*idf inside this catalog, so the terms chosen are the ones that
|
|
109
|
+
* separate this tool from its siblings, with stopwords excluded because in a
|
|
110
|
+
* small catalog a function word repeated in one description can outrank the
|
|
111
|
+
* noun that identifies the tool.
|
|
112
|
+
*/
|
|
113
|
+
export declare function taskQuery(tool: ToolDef, index: Bm25Index, limit?: number): string[];
|
|
114
|
+
export interface FindOptions {
|
|
115
|
+
topK?: number;
|
|
116
|
+
queryTerms?: number;
|
|
117
|
+
/** Claude-style tool-definition tokens, used only to say whether deferral is likely. */
|
|
118
|
+
definitionTokens?: number;
|
|
119
|
+
}
|
|
120
|
+
export declare function analyzeFind(label: string, tools: readonly ToolDef[], opts?: FindOptions): FindResult;
|