@aiwayds/dsh-mcp-adapter 0.5.0 → 0.6.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 +50 -4
- package/README.zh.md +51 -5
- package/lib/index.d.ts +340 -23
- package/lib/index.js +829 -74
- package/lib/index.js.map +1 -1
- package/lib/jev/backends.d.ts +35 -0
- package/lib/jev/backends.js +48 -0
- package/lib/jev/backends.js.map +1 -0
- package/lib/jev/client.d.ts +133 -0
- package/lib/jev/client.js +288 -0
- package/lib/jev/client.js.map +1 -0
- package/lib/jev/config.d.ts +82 -0
- package/lib/jev/config.js +55 -0
- package/lib/jev/config.js.map +1 -0
- package/lib/jev/dual.d.ts +42 -0
- package/lib/jev/dual.js +42 -0
- package/lib/jev/dual.js.map +1 -0
- package/lib/jev/gate.d.ts +51 -0
- package/lib/jev/gate.js +141 -0
- package/lib/jev/gate.js.map +1 -0
- package/lib/jev/log.d.ts +93 -0
- package/lib/jev/log.js +138 -0
- package/lib/jev/log.js.map +1 -0
- package/lib/jev/seams.d.ts +118 -0
- package/lib/jev/seams.js +364 -0
- package/lib/jev/seams.js.map +1 -0
- package/lib/jev/thresholds.d.ts +37 -0
- package/lib/jev/thresholds.js +44 -0
- package/lib/jev/thresholds.js.map +1 -0
- package/lib/rank.d.ts +157 -0
- package/lib/rank.js +327 -0
- package/lib/rank.js.map +1 -0
- package/lib/usage.d.ts +232 -0
- package/lib/usage.js +362 -0
- package/lib/usage.js.map +1 -0
- package/package.json +1 -1
package/lib/rank.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lexical pre-ranking for the two model-facing search seams (`mcp_list
|
|
3
|
+
* {query}` and `mcp_call`'s unknown-tool branch).
|
|
4
|
+
*
|
|
5
|
+
* WHY A SEPARATE MODULE: these are the pure, dependency-free halves of seams
|
|
6
|
+
* B and C. They must stay callable from `node --test` without the meta-tool
|
|
7
|
+
* definitions, AND from `jev/seams.ts` (which reranks whatever this module
|
|
8
|
+
* produced) — so they cannot live in src/index.ts without an import cycle
|
|
9
|
+
* (index.ts → jev/seams.ts → index.ts).
|
|
10
|
+
*
|
|
11
|
+
* CONTRACT (red line ②): everything here is deterministic, synchronous and
|
|
12
|
+
* offline. A jev answer is only ever a REORDER of what these functions already
|
|
13
|
+
* produced (see {@link mergeNameOrder}); any jev failure degrades to the
|
|
14
|
+
* order computed below, so this file is the fail-open baseline of both seams.
|
|
15
|
+
*
|
|
16
|
+
* SCRIPT: the lexical prefilter runs over the ASCII tokens when the query has
|
|
17
|
+
* any, and falls back to a CJK substring match when it has none (see
|
|
18
|
+
* {@link cjkQuery}) — the model asks this plugin's questions in Chinese.
|
|
19
|
+
*
|
|
20
|
+
* @module rank
|
|
21
|
+
*/
|
|
22
|
+
/** Cap on the candidate list handed to a jev batch (both ranking seams). */
|
|
23
|
+
export const RANK_CANDIDATE_LIMIT = 40;
|
|
24
|
+
/** How many ranked tools a `mcp_list {query}` answer actually shows. */
|
|
25
|
+
export const LIST_RESULT_LIMIT = 10;
|
|
26
|
+
/** How many near-misses the unknown-tool error names. */
|
|
27
|
+
export const DID_YOU_MEAN_LIMIT = 3;
|
|
28
|
+
/** Max edit distance a near-miss may have to be offered. */
|
|
29
|
+
export const DID_YOU_MEAN_MAX_DISTANCE = 3;
|
|
30
|
+
/**
|
|
31
|
+
* Lowercase alphanumeric tokens of one string, split on non-alphanumeric runs
|
|
32
|
+
* AND on camelCase / letter-digit boundaries — so `create_issue`,
|
|
33
|
+
* `createIssue` and `createissue` all reduce to overlapping tokens. Tool
|
|
34
|
+
* naming conventions vary per MCP vendor; the search seam must not depend on
|
|
35
|
+
* one of them.
|
|
36
|
+
*
|
|
37
|
+
* ASCII ONLY, on purpose: the token weights in {@link scoreQueryMatch} are
|
|
38
|
+
* calibrated on token equality, and a CJK query has no ASCII tokens to
|
|
39
|
+
* compare. Chinese/other CJK text is matched by the substring fallback
|
|
40
|
+
* ({@link cjkQuery}) instead, and that fallback is a SEPARATE path so the
|
|
41
|
+
* ASCII ranking cannot drift.
|
|
42
|
+
*/
|
|
43
|
+
export function tokenize(text) {
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const chunk of text.split(/[^A-Za-z0-9]+/)) {
|
|
46
|
+
if (chunk === '')
|
|
47
|
+
continue;
|
|
48
|
+
for (const part of chunk.split(/(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Za-z])(?=[0-9])/)) {
|
|
49
|
+
if (part !== '')
|
|
50
|
+
out.push(part.toLowerCase());
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/** All alphanumerics of one string, lowercased (`create_issue` → `createissue`). */
|
|
56
|
+
function compact(text) {
|
|
57
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/gu, '');
|
|
58
|
+
}
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// CJK fallback (added 09-27). dsh is a Chinese-language ecosystem, so the
|
|
61
|
+
// model's `mcp_list {query}` is very often Chinese — and Chinese has no
|
|
62
|
+
// spaces, no case and no ASCII letters, so tokenize() returns NOTHING and the
|
|
63
|
+
// prefilter reported "no MCP tool matches the query" for a query it had never
|
|
64
|
+
// actually searched. The fallback is a substring match over the query's CJK
|
|
65
|
+
// runs, kept out of the ASCII path so the ASCII weights stay exactly as
|
|
66
|
+
// calibrated.
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
/** One run of Han / kana / Hangul text. */
|
|
69
|
+
const CJK_RUN = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]+/gu;
|
|
70
|
+
/** The CJK content of one string, with everything else dropped. */
|
|
71
|
+
function cjkText(text) {
|
|
72
|
+
return (text.match(CJK_RUN) ?? []).join('');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Reduce a query to its CJK content, or null when it has none (an ASCII or
|
|
76
|
+
* punctuation-only query takes the token path instead).
|
|
77
|
+
*
|
|
78
|
+
* BIGRAMS, not single characters: they are the Chinese analogue of an ASCII
|
|
79
|
+
* word, so `搜索文件` searches 搜索 / 索文 / 文件 and a description that
|
|
80
|
+
* merely happens to contain 文 is not a match. A one-character run has no
|
|
81
|
+
* bigram and stands for itself, so `文` still finds `读取文件`.
|
|
82
|
+
*/
|
|
83
|
+
function cjkQuery(query) {
|
|
84
|
+
const text = cjkText(query);
|
|
85
|
+
if (text === '')
|
|
86
|
+
return null;
|
|
87
|
+
const tokens = [];
|
|
88
|
+
for (const run of query.match(CJK_RUN) ?? []) {
|
|
89
|
+
if (run.length < 2) {
|
|
90
|
+
tokens.push(run);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
for (let i = 0; i + 1 < run.length; i += 1)
|
|
94
|
+
tokens.push(run.slice(i, i + 2));
|
|
95
|
+
}
|
|
96
|
+
return { text, tokens };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The CJK counterpart of {@link scoreQueryMatch}: substring, not equality —
|
|
100
|
+
* a CJK tool name is compared as a whole, then bigram-by-bigram against the
|
|
101
|
+
* NAME before the DESCRIPTION, with the same weights as the ASCII path so the
|
|
102
|
+
* two rank a mixed catalog the same way.
|
|
103
|
+
*
|
|
104
|
+
* Latin text is stripped from both sides: it cannot carry a CJK query, and
|
|
105
|
+
* leaving it in would let `读取文件` match the latin bytes of a name.
|
|
106
|
+
*/
|
|
107
|
+
function scoreCjkMatch(entry, query) {
|
|
108
|
+
const name = cjkText(entry.name);
|
|
109
|
+
if (name !== '' && name === query.text)
|
|
110
|
+
return 1000;
|
|
111
|
+
const description = cjkText(entry.description);
|
|
112
|
+
let score = 0;
|
|
113
|
+
for (const token of query.tokens) {
|
|
114
|
+
if (name.includes(token))
|
|
115
|
+
score += 4;
|
|
116
|
+
else if (description.includes(token))
|
|
117
|
+
score += 1;
|
|
118
|
+
}
|
|
119
|
+
if (query.tokens.length > 1 && name.includes(query.text))
|
|
120
|
+
score += 8;
|
|
121
|
+
return score;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Lexical score of one entry against a tokenized query. 0 means "no match at
|
|
125
|
+
* all" and is what the rankers filter on — an unmatched entry is never shown
|
|
126
|
+
* as a result, however long its description is.
|
|
127
|
+
*
|
|
128
|
+
* Weights: exact full-name equality dominates; a query token found in the
|
|
129
|
+
* NAME beats one found only in the DESCRIPTION; a whole-phrase substring hit
|
|
130
|
+
* beats a scattered per-token hit.
|
|
131
|
+
*
|
|
132
|
+
* @param entry - the tool being scored.
|
|
133
|
+
* @param tokens - query tokens from {@link tokenize}.
|
|
134
|
+
* @param queryCompact - the whole query squashed to alphanumerics.
|
|
135
|
+
* @returns the score (0 = no match).
|
|
136
|
+
*/
|
|
137
|
+
export function scoreQueryMatch(entry, tokens, queryCompact) {
|
|
138
|
+
const nameCompact = compact(entry.name);
|
|
139
|
+
if (nameCompact === queryCompact && queryCompact !== '')
|
|
140
|
+
return 1000;
|
|
141
|
+
const nameTokens = new Set(tokenize(entry.name));
|
|
142
|
+
const descriptionTokens = new Set(tokenize(entry.description));
|
|
143
|
+
// Vendors ship glued names in prose ("Create an issue on GitHub"), where the
|
|
144
|
+
// token splitter sees git+hub. The squashed description is the last resort
|
|
145
|
+
// so a query for "github" still finds them — at description weight.
|
|
146
|
+
const descriptionCompact = compact(entry.description);
|
|
147
|
+
let score = 0;
|
|
148
|
+
for (const token of tokens) {
|
|
149
|
+
if (nameTokens.has(token))
|
|
150
|
+
score += 4;
|
|
151
|
+
else if (nameCompact.includes(token))
|
|
152
|
+
score += 2;
|
|
153
|
+
else if (descriptionTokens.has(token))
|
|
154
|
+
score += 1;
|
|
155
|
+
else if (descriptionCompact.includes(token))
|
|
156
|
+
score += 1;
|
|
157
|
+
}
|
|
158
|
+
if (tokens.length > 1 && queryCompact !== '' && nameCompact.includes(queryCompact))
|
|
159
|
+
score += 8;
|
|
160
|
+
return score;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Rank entries against a free-text query: best score first, name ascending as
|
|
164
|
+
* the deterministic tie-break, entries with no lexical match dropped, at most
|
|
165
|
+
* `limit` survivors.
|
|
166
|
+
*
|
|
167
|
+
* A query with no ASCII token falls back to the CJK substring prefilter
|
|
168
|
+
* ({@link cjkQuery}) — the model writes this plugin's queries in Chinese, and
|
|
169
|
+
* "no tokens" must not read as "nothing matched". A query with neither ASCII
|
|
170
|
+
* nor CJK content (blank, punctuation) still matches nothing.
|
|
171
|
+
*
|
|
172
|
+
* Pure and synchronous — this is the order a failed/failed-open jev rerank
|
|
173
|
+
* degrades to, and the only order produced when `jevEnabled` is false.
|
|
174
|
+
*
|
|
175
|
+
* @param query - the model's raw query text.
|
|
176
|
+
* @param entries - the catalog entries to score (input is never mutated).
|
|
177
|
+
* @param limit - max survivors; omit for "every match".
|
|
178
|
+
* @returns the surviving entries, best first.
|
|
179
|
+
*/
|
|
180
|
+
export function rankByQuery(query, entries, limit = Number.POSITIVE_INFINITY) {
|
|
181
|
+
const tokens = tokenize(query);
|
|
182
|
+
const queryCompact = compact(query);
|
|
183
|
+
// Only a query the ASCII tokenizer cannot see at all takes the CJK path;
|
|
184
|
+
// a mixed query keeps the (calibrated) ASCII ranking it always had.
|
|
185
|
+
const cjk = tokens.length === 0 ? cjkQuery(query) : null;
|
|
186
|
+
if (tokens.length === 0 && cjk === null)
|
|
187
|
+
return [];
|
|
188
|
+
const scored = [];
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
const score = cjk === null ? scoreQueryMatch(entry, tokens, queryCompact) : scoreCjkMatch(entry, cjk);
|
|
191
|
+
if (score > 0)
|
|
192
|
+
scored.push({ entry, score });
|
|
193
|
+
}
|
|
194
|
+
scored.sort((left, right) => (right.score - left.score) || left.entry.name.localeCompare(right.entry.name));
|
|
195
|
+
return scored.slice(0, limit === Number.POSITIVE_INFINITY ? undefined : limit).map(item => item.entry);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Reorder a list of NAMES by an external ranking (the jev answer). Names the
|
|
199
|
+
* ranking does not mention — and names it repeats — keep their original
|
|
200
|
+
* relative order at the tail, so a short or partial answer degrades to "jev's
|
|
201
|
+
* prefix + lexical remainder" instead of dropping candidates. Names that were
|
|
202
|
+
* not in `ordered` at all are ignored.
|
|
203
|
+
*
|
|
204
|
+
* @param ordered - the lexical order being permuted.
|
|
205
|
+
* @param ranking - tool names, best first, as returned by the reranker.
|
|
206
|
+
* @returns a new array; `ordered` is never mutated.
|
|
207
|
+
*/
|
|
208
|
+
export function mergeNameOrder(ordered, ranking) {
|
|
209
|
+
const known = new Set(ordered);
|
|
210
|
+
const out = [];
|
|
211
|
+
const taken = new Set();
|
|
212
|
+
for (const name of ranking) {
|
|
213
|
+
if (!known.has(name) || taken.has(name))
|
|
214
|
+
continue;
|
|
215
|
+
taken.add(name);
|
|
216
|
+
out.push(name);
|
|
217
|
+
}
|
|
218
|
+
for (const name of ordered)
|
|
219
|
+
if (!taken.has(name))
|
|
220
|
+
out.push(name);
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Levenshtein distance between two names, or `undefined` when it is provably
|
|
225
|
+
* greater than `max`. The length pre-check plus the per-row minimum make the
|
|
226
|
+
* common "clearly not a near-miss" case O(max) instead of O(n·m); names are
|
|
227
|
+
* short anyway, but the pool can be a full catalog and this runs inside a
|
|
228
|
+
* dispatch error path.
|
|
229
|
+
*
|
|
230
|
+
* @param a - first name (case-insensitive).
|
|
231
|
+
* @param b - second name (case-insensitive).
|
|
232
|
+
* @param max - distances above this report "no".
|
|
233
|
+
* @returns the distance, or undefined when it exceeds `max`.
|
|
234
|
+
*/
|
|
235
|
+
export function editDistanceWithin(a, b, max) {
|
|
236
|
+
const left = a.toLowerCase();
|
|
237
|
+
const right = b.toLowerCase();
|
|
238
|
+
if (left === right)
|
|
239
|
+
return 0;
|
|
240
|
+
if (Math.abs(left.length - right.length) > max)
|
|
241
|
+
return undefined;
|
|
242
|
+
let previous = Array.from({ length: right.length + 1 }, (_unused, index) => index);
|
|
243
|
+
for (let row = 1; row <= left.length; row += 1) {
|
|
244
|
+
const current = [row];
|
|
245
|
+
let rowMin = row;
|
|
246
|
+
for (let column = 1; column <= right.length; column += 1) {
|
|
247
|
+
const cost = left[row - 1] === right[column - 1] ? 0 : 1;
|
|
248
|
+
const value = Math.min(previous[column] + 1, current[column - 1] + 1, previous[column - 1] + cost);
|
|
249
|
+
current.push(value);
|
|
250
|
+
if (value < rowMin)
|
|
251
|
+
rowMin = value;
|
|
252
|
+
}
|
|
253
|
+
if (rowMin > max)
|
|
254
|
+
return undefined;
|
|
255
|
+
previous = current;
|
|
256
|
+
}
|
|
257
|
+
const distance = previous[right.length];
|
|
258
|
+
return distance <= max ? distance : undefined;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Lexical near-miss candidates for one unregistered tool name: everything
|
|
262
|
+
* within {@link DID_YOU_MEAN_MAX_DISTANCE} edits, plus anything sharing a
|
|
263
|
+
* name token, best first. This is the always-on floor of the unknown-tool
|
|
264
|
+
* branch — it runs whether or not jev is enabled, needs no network, and is
|
|
265
|
+
* the order that stands when a jev rerank fails.
|
|
266
|
+
*
|
|
267
|
+
* @param tool - the name dispatch could not resolve.
|
|
268
|
+
* @param pool - candidate names (production: the visible prefix tools).
|
|
269
|
+
* @param limit - max candidates (default {@link DID_YOU_MEAN_LIMIT}).
|
|
270
|
+
* @param ignoreTokens - tokens that must NOT count as a shared-token match.
|
|
271
|
+
* The caller's fold prefix goes here: every candidate in the pool starts
|
|
272
|
+
* with it, so "shares mcp" is not evidence of anything.
|
|
273
|
+
* @returns the candidates, best first (edit tier, then fewest edits, then
|
|
274
|
+
* most shared tokens, then shorter, then alphabetical).
|
|
275
|
+
*/
|
|
276
|
+
export function didYouMeanCandidates(tool, pool, limit = DID_YOU_MEAN_LIMIT, ignoreTokens = []) {
|
|
277
|
+
const ignored = new Set(ignoreTokens);
|
|
278
|
+
const toolTokens = new Set(tokenize(tool).filter(token => !ignored.has(token)));
|
|
279
|
+
const scored = [];
|
|
280
|
+
for (const name of pool) {
|
|
281
|
+
if (name === tool)
|
|
282
|
+
continue;
|
|
283
|
+
const distance = editDistanceWithin(tool, name, DID_YOU_MEAN_MAX_DISTANCE);
|
|
284
|
+
if (distance !== undefined) {
|
|
285
|
+
scored.push({ name, tier: 'edit', distance, shared: sharedTokens(toolTokens, name) });
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const shared = sharedTokens(toolTokens, name);
|
|
289
|
+
if (shared > 0)
|
|
290
|
+
scored.push({ name, tier: 'token', distance: 0, shared });
|
|
291
|
+
}
|
|
292
|
+
scored.sort((left, right) => (tierRank(left.tier) - tierRank(right.tier))
|
|
293
|
+
|| (left.distance - right.distance)
|
|
294
|
+
|| (right.shared - left.shared)
|
|
295
|
+
|| (left.name.length - right.name.length)
|
|
296
|
+
|| left.name.localeCompare(right.name));
|
|
297
|
+
return scored.slice(0, limit === Number.POSITIVE_INFINITY ? undefined : limit);
|
|
298
|
+
}
|
|
299
|
+
/** Edit-tier candidates always outrank token-only ones. */
|
|
300
|
+
function tierRank(tier) {
|
|
301
|
+
return tier === 'edit' ? 0 : 1;
|
|
302
|
+
}
|
|
303
|
+
/** How many distinct tokens the candidate's name shares with the mistyped one. */
|
|
304
|
+
function sharedTokens(toolTokens, name) {
|
|
305
|
+
let shared = 0;
|
|
306
|
+
for (const token of new Set(tokenize(name)))
|
|
307
|
+
if (toolTokens.has(token))
|
|
308
|
+
shared += 1;
|
|
309
|
+
return shared;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* The suffix appended to the unknown-tool error. The base message stays
|
|
313
|
+
* byte-identical to the pre-seam one, so a caller that only knew the old text
|
|
314
|
+
* still matches it; the suggestion is an addition, and its provenance is
|
|
315
|
+
* always stated (an unranked guess must not look like a ranked one).
|
|
316
|
+
*
|
|
317
|
+
* @param names - suggested tool names, best first.
|
|
318
|
+
* @param source - who produced the order.
|
|
319
|
+
* @returns `''` when there is nothing to suggest.
|
|
320
|
+
*/
|
|
321
|
+
export function didYouMeanMessage(names, source) {
|
|
322
|
+
if (names.length === 0)
|
|
323
|
+
return '';
|
|
324
|
+
const label = source === 'jev' ? 'jev reranked' : 'lexical';
|
|
325
|
+
return ` Did you mean: ${names.map(name => `"${name}"`).join(', ')}? (${label})`;
|
|
326
|
+
}
|
|
327
|
+
//# sourceMappingURL=rank.js.map
|
package/lib/rank.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rank.js","sourceRoot":"","sources":["../src/rank.ts"],"names":[],"mappings":"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"}
|
package/lib/usage.d.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-tool usage statistics and the keep-suggestion document (seam A — the
|
|
3
|
+
* self-evolution core).
|
|
4
|
+
*
|
|
5
|
+
* WHAT THIS MODULE OWNS: the durable facts only.
|
|
6
|
+
* - `usage.json` — `{ tools: { <name>: { calls, errors, firstUsedAt,
|
|
7
|
+
* lastUsedAt } } }`, one document in the plugin's own gate directory
|
|
8
|
+
* (beside gate.json / decisions.jsonl / suggestions.json);
|
|
9
|
+
* - `suggestions.json` — the last jev keep-suggestion batch.
|
|
10
|
+
*
|
|
11
|
+
* WHAT IT DELIBERATELY DOES NOT OWN: any jev call. The batch that fills
|
|
12
|
+
* suggestions.json lives in `jev/seams.ts`; this module only knows the shape
|
|
13
|
+
* of its result (so the command surface can render it without importing the
|
|
14
|
+
* client). Keeping the decision out of here is what makes the record path
|
|
15
|
+
* synchronous and remote-free: `record()` updates memory and schedules a
|
|
16
|
+
* write, and returns. Red line ① (no jev on the hot path) holds structurally
|
|
17
|
+
* because this file cannot call out at all.
|
|
18
|
+
*
|
|
19
|
+
* FAIL-OPEN (red line ②): a corrupt or unreadable document is warned about
|
|
20
|
+
* once and degrades to zero statistics — never to a crash and never to a
|
|
21
|
+
* wedged dispatch. Suggestions that cannot be read degrade to "no
|
|
22
|
+
* suggestions yet", which is exactly what an empty file means.
|
|
23
|
+
*
|
|
24
|
+
* @module usage
|
|
25
|
+
*/
|
|
26
|
+
import type { JevBand } from './jev/thresholds.ts';
|
|
27
|
+
/** The usage document file name inside the plugin's gate directory. */
|
|
28
|
+
export declare const USAGE_FILE_NAME = "usage.json";
|
|
29
|
+
/** The keep-suggestion document file name inside the plugin's gate directory. */
|
|
30
|
+
export declare const SUGGESTIONS_FILE_NAME = "suggestions.json";
|
|
31
|
+
/** Absolute usage.json path for one gate directory. */
|
|
32
|
+
export declare function usageFile(dir: string): string;
|
|
33
|
+
/** Absolute suggestions.json path for one gate directory. */
|
|
34
|
+
export declare function suggestionsFile(dir: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Recorded counters for one tool. Timestamps are ISO strings (`''` when the
|
|
37
|
+
* document was hand-written without them) so the document stays
|
|
38
|
+
* human-readable and diffable.
|
|
39
|
+
*/
|
|
40
|
+
export interface ToolUsageStats {
|
|
41
|
+
/** Dispatches through mcp_call, successes AND failures. */
|
|
42
|
+
calls: number;
|
|
43
|
+
/** How many of those dispatches came back as this plugin's `{ error }` wrap. */
|
|
44
|
+
errors: number;
|
|
45
|
+
/** ISO time of the first recorded call; `''` when unknown. */
|
|
46
|
+
firstUsedAt: string;
|
|
47
|
+
/** ISO time of the most recent recorded call; `''` when unknown. */
|
|
48
|
+
lastUsedAt: string;
|
|
49
|
+
}
|
|
50
|
+
/** The whole usage document. */
|
|
51
|
+
export interface UsageDocument {
|
|
52
|
+
tools: Record<string, ToolUsageStats>;
|
|
53
|
+
}
|
|
54
|
+
/** Fresh-install zero state. */
|
|
55
|
+
export declare const EMPTY_USAGE: UsageDocument;
|
|
56
|
+
/** The warning seam (production: `ctx.logger.warn`; tests: a collector). */
|
|
57
|
+
export type UsageWarn = (message: string) => void;
|
|
58
|
+
/**
|
|
59
|
+
* Sanitize any parsed usage document. Entry-by-entry: a corrupt counter
|
|
60
|
+
* becomes zero, a corrupt timestamp becomes `''`, and a non-object document
|
|
61
|
+
* becomes the empty one. Hand-edited or half-written files therefore cost
|
|
62
|
+
* precision, never correctness.
|
|
63
|
+
*
|
|
64
|
+
* @param raw - the parsed JSON (or anything at all).
|
|
65
|
+
* @returns a fresh document; the input is never retained.
|
|
66
|
+
*/
|
|
67
|
+
export declare function normalizeUsage(raw: unknown): UsageDocument;
|
|
68
|
+
/**
|
|
69
|
+
* Fold one dispatch into a document and return the NEXT document (pure — the
|
|
70
|
+
* input object and its stats are never mutated, so a caller holding a snapshot
|
|
71
|
+
* keeps it). `firstUsedAt` is stamped on the tool's first call only, so it
|
|
72
|
+
* stays a true "when did this tool start being used" answer.
|
|
73
|
+
*
|
|
74
|
+
* @param doc - the current document.
|
|
75
|
+
* @param tool - the dispatched tool name.
|
|
76
|
+
* @param failed - whether the dispatch came back as a structured error.
|
|
77
|
+
* @param now - ISO timestamp to stamp (default: now).
|
|
78
|
+
* @returns the next document.
|
|
79
|
+
*/
|
|
80
|
+
export declare function recordUsageCall(doc: UsageDocument, tool: string, failed: boolean, now?: string): UsageDocument;
|
|
81
|
+
/** Total dispatches recorded across every tool. */
|
|
82
|
+
export declare function usageTotalCalls(doc: UsageDocument): number;
|
|
83
|
+
/** Number of tools carrying at least one recorded call. */
|
|
84
|
+
export declare function usageToolCount(doc: UsageDocument): number;
|
|
85
|
+
/**
|
|
86
|
+
* How many total calls pass before a suggestion batch may fire.
|
|
87
|
+
* Deliberately coarse: the batch is a remote round trip whose answers are
|
|
88
|
+
* about prompt economics, not correctness, so it runs on a slow cadence
|
|
89
|
+
* instead of per call.
|
|
90
|
+
*/
|
|
91
|
+
export declare const SUGGEST_BATCH_EVERY = 20;
|
|
92
|
+
/**
|
|
93
|
+
* Whether crossing into `totalCalls` opens a new suggestion batch. True only
|
|
94
|
+
* on the call that MAKES a multiple (20, 40, …), so N calls trigger exactly
|
|
95
|
+
* floor(N/20) batches and never more — the threshold predicate is a pure
|
|
96
|
+
* function precisely so the hot path can ask it without any jev work.
|
|
97
|
+
*
|
|
98
|
+
* @param totalCalls - total calls AFTER the just-recorded one.
|
|
99
|
+
* @param every - batch cadence (default {@link SUGGEST_BATCH_EVERY}).
|
|
100
|
+
* @returns whether a batch should be started.
|
|
101
|
+
*/
|
|
102
|
+
export declare function shouldRunSuggestBatch(totalCalls: number, every?: number): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* The live usage handle. Reads are served from the in-memory mirror and NEVER
|
|
105
|
+
* touch the disk; `record` updates the mirror synchronously (so a caller can
|
|
106
|
+
* threshold on the total it just produced) and schedules an atomic write.
|
|
107
|
+
*
|
|
108
|
+
* WRITE POLICY — write-through with coalescing: the first record starts a
|
|
109
|
+
* write; records arriving while one is in flight are folded into ONE follow-up
|
|
110
|
+
* write carrying the newest state. So the document on disk is at most one
|
|
111
|
+
* write cycle behind the mirror, and a crash can lose only the records counted
|
|
112
|
+
* during that single in-flight write (milliseconds, one tmp+rename pair) — not
|
|
113
|
+
* an unbounded tail. `flush()` awaits the queue for shutdown/tests.
|
|
114
|
+
*/
|
|
115
|
+
export interface UsageStore {
|
|
116
|
+
/** Current mirror (corrupt input already sanitized away). */
|
|
117
|
+
get(): UsageDocument;
|
|
118
|
+
/** Total calls in the mirror. */
|
|
119
|
+
total(): number;
|
|
120
|
+
/** Count one dispatch; returns the new total. Never throws. */
|
|
121
|
+
record(tool: string, failed: boolean): number;
|
|
122
|
+
/** Await the pending write(s), if any. */
|
|
123
|
+
flush(): Promise<void>;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Build the usage store over one gate directory. The initial read is
|
|
127
|
+
* synchronous (boot-time, once, mirroring the gate store); writes land
|
|
128
|
+
* atomically (tmp file + rename) so a crash mid-write can never leave a torn
|
|
129
|
+
* document.
|
|
130
|
+
*
|
|
131
|
+
* @param dir - the plugin's gate directory (`resolveGateDir(...)`).
|
|
132
|
+
* @param warn - warning seam for a corrupt document / failed write.
|
|
133
|
+
* @returns the store handle.
|
|
134
|
+
*/
|
|
135
|
+
export declare function createUsageStore(dir: string, warn: UsageWarn): UsageStore;
|
|
136
|
+
/**
|
|
137
|
+
* One suggestion row. `basis` is the human-readable usage evidence the
|
|
138
|
+
* suggestion was derived from (calls / errors / recency) — the row is a
|
|
139
|
+
* PROPOSAL about prompt economics, never an applied config change (red line
|
|
140
|
+
* ③: the model face writes nothing, and neither does the operator's
|
|
141
|
+
* settings — `/mcp suggest` only renders).
|
|
142
|
+
*/
|
|
143
|
+
export interface SuggestionRecord {
|
|
144
|
+
/** The folded tool the row is about. */
|
|
145
|
+
tool: string;
|
|
146
|
+
/** jev's calibrated probability that it is worth keeping in every prompt. */
|
|
147
|
+
probability: number;
|
|
148
|
+
/** Band the probability fell into (thresholds.ts, kind 'suggest'). */
|
|
149
|
+
band: JevBand;
|
|
150
|
+
/** Usage evidence, e.g. `24 calls, 1 error, last used 2026-09-27`. */
|
|
151
|
+
basis: string;
|
|
152
|
+
/** ISO time the batch produced this row. */
|
|
153
|
+
_at: string;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Sanitize a parsed suggestions document into rows that are safe to render.
|
|
157
|
+
* Rows without a name or a usable probability are dropped rather than shown as
|
|
158
|
+
* "0%" — a fabricated number is worse than a missing row.
|
|
159
|
+
*
|
|
160
|
+
* @param raw - the parsed JSON (an array of rows, or anything at all).
|
|
161
|
+
* @returns the renderable rows, in document order.
|
|
162
|
+
*/
|
|
163
|
+
export declare function normalizeSuggestions(raw: unknown): SuggestionRecord[];
|
|
164
|
+
/**
|
|
165
|
+
* Replace the whole suggestion document with one batch (atomic tmp+rename;
|
|
166
|
+
* a crash leaves either the previous batch or the new one, never a mix).
|
|
167
|
+
* Throws on failure — the caller is the fire-and-forget seam, which contains
|
|
168
|
+
* it; a failed write means "this round's suggestions are lost", never a broken
|
|
169
|
+
* caller.
|
|
170
|
+
*
|
|
171
|
+
* @param dir - the plugin's gate directory.
|
|
172
|
+
* @param records - the batch rows.
|
|
173
|
+
*/
|
|
174
|
+
export declare function writeSuggestions(dir: string, records: readonly SuggestionRecord[]): Promise<void>;
|
|
175
|
+
/**
|
|
176
|
+
* Read the current suggestions (empty when there are none, unreadable, or
|
|
177
|
+
* corrupt — the display layer's "no suggestions yet" is the same shape as
|
|
178
|
+
* "the file was lost", so the operator is never shown a half-truth).
|
|
179
|
+
*
|
|
180
|
+
* @param dir - the plugin's gate directory.
|
|
181
|
+
* @param warn - optional warning seam for an unreadable document.
|
|
182
|
+
* @returns the renderable rows.
|
|
183
|
+
*/
|
|
184
|
+
export declare function readSuggestions(dir: string, warn?: UsageWarn): Promise<SuggestionRecord[]>;
|
|
185
|
+
/** One tool the keep-suggestion batch may ask about. */
|
|
186
|
+
export interface SuggestCandidate {
|
|
187
|
+
/** The folded tool name. */
|
|
188
|
+
tool: string;
|
|
189
|
+
/** Recorded dispatches — the evidence a "keep" claim must rest on. */
|
|
190
|
+
calls: number;
|
|
191
|
+
/** How many of them failed. */
|
|
192
|
+
errors: number;
|
|
193
|
+
/** ISO time of the most recent call. */
|
|
194
|
+
lastUsedAt: string;
|
|
195
|
+
/** First {@link SUGGEST_DESCRIPTION_CHARS} chars of the description. */
|
|
196
|
+
description: string;
|
|
197
|
+
}
|
|
198
|
+
/** How much of a tool's description travels into the jev state text. */
|
|
199
|
+
export declare const SUGGEST_DESCRIPTION_CHARS = 200;
|
|
200
|
+
/** The minimal shape a folded-tool pool must have. */
|
|
201
|
+
export interface DescribableSchema {
|
|
202
|
+
readonly name: string;
|
|
203
|
+
readonly description: string;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The candidate set for one keep-suggestion batch: the tools that are
|
|
207
|
+
* CURRENTLY FOLDED OUT of the prompt (the fold predicate is injected, so this
|
|
208
|
+
* function stays free of config plumbing) AND have at least one recorded call.
|
|
209
|
+
*
|
|
210
|
+
* A folded tool nobody ever called is deliberately excluded: it has no usage
|
|
211
|
+
* evidence to build a "keep this in every prompt" claim on, and burning a
|
|
212
|
+
* remote question on it would put the least informative tools first. Ordered
|
|
213
|
+
* by calls descending (the tools that already earn their place come first),
|
|
214
|
+
* name ascending as the tie-break, capped at {@link RANK_CANDIDATE_LIMIT}.
|
|
215
|
+
*
|
|
216
|
+
* @param doc - the usage mirror.
|
|
217
|
+
* @param schemas - visible schemas (production: `ctx.tools.schemas()`).
|
|
218
|
+
* @param isFolded - the live fold predicate for one name.
|
|
219
|
+
* @param limit - max candidates (default {@link RANK_CANDIDATE_LIMIT}).
|
|
220
|
+
* @returns the candidates, most-used first.
|
|
221
|
+
*/
|
|
222
|
+
export declare function usageSuggestCandidates(doc: UsageDocument, schemas: readonly DescribableSchema[], isFolded: (name: string) => boolean, limit?: number): SuggestCandidate[];
|
|
223
|
+
/** The usage evidence rendered next to a suggestion, in one line. */
|
|
224
|
+
export declare function suggestionBasis(candidate: Pick<SuggestCandidate, 'calls' | 'errors' | 'lastUsedAt'>): string;
|
|
225
|
+
/**
|
|
226
|
+
* The exact `keep` config fragment a suggestion would translate to. Rendered
|
|
227
|
+
* as TEXT for the operator — nothing writes it (red line ③).
|
|
228
|
+
*
|
|
229
|
+
* @param tool - the suggested tool name.
|
|
230
|
+
* @returns a copy-pasteable keep-pattern line.
|
|
231
|
+
*/
|
|
232
|
+
export declare function keepSuggestionFragment(tool: string): string;
|