dsh-tool-folder 0.2.0 → 0.2.1
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 +13 -0
- package/lib/index.js +24 -7
- package/lib/metrics.js +65 -0
- package/package.json +48 -48
package/README.md
CHANGED
|
@@ -149,6 +149,19 @@ were never unregistered, so nothing else changes.
|
|
|
149
149
|
|
|
150
150
|
## Changelog
|
|
151
151
|
|
|
152
|
+
### v0.2.1 — heat decay + exact-name priority (2026-08-30)
|
|
153
|
+
1. **Hot heat decay** (`hotWindowDays`, default 3): a tool is promoted to
|
|
154
|
+
always-visible only when its call count stays inside the sliding window —
|
|
155
|
+
the old code promoted forever, so any tool used 3 times was permanently
|
|
156
|
+
resident and the fold silently decayed to zero over time. Legacy
|
|
157
|
+
`feedback.json` number entries are read as out-of-window → their stale
|
|
158
|
+
heat expires naturally. Set `hotWindowDays: 0` to keep the old behavior.
|
|
159
|
+
2. **Exact-name priority in `tools_search`**: exact tool-name match always
|
|
160
|
+
ranks first, a ≥4-char query that is a name substring is pulled ahead of
|
|
161
|
+
BM25/semantic hits (deterministic fallback chain). Results stay
|
|
162
|
+
deduplicated.
|
|
163
|
+
3. 40 tests green (11 new: window decay / legacy compat / priority order).
|
|
164
|
+
|
|
152
165
|
### v0.2.0 — first npm release (2026-08-30)
|
|
153
166
|
This is the first version published to npm. It bundles the v0.1.7 firewall
|
|
154
167
|
scope fix (exec-only hard block) and the v0.1.8 semantic leg +
|
package/lib/index.js
CHANGED
|
@@ -33,7 +33,7 @@ import path from "node:path";
|
|
|
33
33
|
import z from "@deepseek-ai/schemastery";
|
|
34
34
|
import { buildIndex, search } from "./bm25.js";
|
|
35
35
|
import { compressTool, normalizeDescription, sanitizeLossless } from "./schema.js";
|
|
36
|
-
import { selectionCoverage } from "./metrics.js";
|
|
36
|
+
import { selectionCoverage, isHotTool, prioritizeExact } from "./metrics.js";
|
|
37
37
|
import { ChainGuard, verdict } from "./chainguard.js";
|
|
38
38
|
import { matchCategory } from "./category.js";
|
|
39
39
|
import { toonifyValue } from "./toonify.js";
|
|
@@ -59,6 +59,8 @@ export const Config = z.object({
|
|
|
59
59
|
deny: z.array(z.string()).default([]).description("禁用工具:精确名或 prefix* 前缀,命中不注入+拒绝执行"),
|
|
60
60
|
topK: z.number().min(0).max(50).default(6).description("BM25 动态检索每轮最多注入的工具数"),
|
|
61
61
|
hotThreshold: z.number().min(1).default(3).description("被折叠工具调用几次后自动晋升为常驻"),
|
|
62
|
+
hotWindowDays: z.number().min(0).max(365).default(3)
|
|
63
|
+
.description("hot 热度的滑动窗口(天):窗口内达标才常驻,超窗自动衰减;0=永不过期"),
|
|
62
64
|
catalogEnabled: z.boolean().default(true).description("折叠安全网:把被藏工具列表写进提示词,模型知道它们存在"),
|
|
63
65
|
compressLevel: z.union(COMPRESS_LEVELS).default("off").description("L1 schema 压缩级别:off 不压缩 / light 裁描述 / standard 裁描述+参数说明 / aggressive 删可选参数"),
|
|
64
66
|
compressEnabled: z.boolean().default(false).description("兼容开关:compressLevel=off 且此开=true 时按 standard 处理"),
|
|
@@ -84,6 +86,7 @@ const DEFAULTS = {
|
|
|
84
86
|
deny: [], // never inject + refuse execution (exact name or "prefix*")
|
|
85
87
|
topK: 6,
|
|
86
88
|
hotThreshold: 3,
|
|
89
|
+
hotWindowDays: 3, // v0.2.1: sliding-window heat decay (0 = never expire)
|
|
87
90
|
catalogEnabled: true, // safety net: folded tools stay discoverable by name
|
|
88
91
|
compressEnabled: false, // deprecated compat: compressLevel "off" + true → "standard"
|
|
89
92
|
compressLevel: "off", // "off" | "light" | "standard" | "aggressive"
|
|
@@ -403,7 +406,13 @@ export function apply(ctx, config) {
|
|
|
403
406
|
const q = String(args.query || "");
|
|
404
407
|
const lim = args.limit ?? 8;
|
|
405
408
|
if (!index || !q) return { query: q, results: [], total: 0 };
|
|
406
|
-
|
|
409
|
+
// v0.2.1: exact-name > name-substring > BM25 priority (deterministic
|
|
410
|
+
// fallback chain, borrowed from Hermes-style tool search).
|
|
411
|
+
const bm25Hits = prioritizeExact(
|
|
412
|
+
allTools,
|
|
413
|
+
q,
|
|
414
|
+
search(index, q, lim * 2).map((i) => ({ id: allTools[i].name })),
|
|
415
|
+
);
|
|
407
416
|
let fused = bm25Hits;
|
|
408
417
|
// Semantic leg (P0-1): RRF-fuse BM25 with local bge-m3 similarity.
|
|
409
418
|
// Any failure (Ollama down / timeout / cache miss on error) degrades
|
|
@@ -519,10 +528,12 @@ export function apply(ctx, config) {
|
|
|
519
528
|
|
|
520
529
|
const agentId = context?.agent?.id || "default";
|
|
521
530
|
const coreNames = new Set(cfg.perAgent?.[agentId]?.core || cfg.core || []);
|
|
531
|
+
// v0.2.1: hot = sliding-window decay (hotWindowDays). Tools not called
|
|
532
|
+
// inside the window lose their historical heat — the old code promoted
|
|
533
|
+
// forever, which slowly nullified the fold.
|
|
522
534
|
const hotNames = new Set(
|
|
523
|
-
Object.
|
|
524
|
-
.
|
|
525
|
-
.map(([n]) => n),
|
|
535
|
+
Object.keys(feedback.calls || {}).filter((n) =>
|
|
536
|
+
isHotTool(feedback.calls, n, cfg.hotThreshold, cfg.hotWindowDays * 86400000)),
|
|
526
537
|
);
|
|
527
538
|
|
|
528
539
|
const stamp = tools.map((t) => t.name).join("\u0001");
|
|
@@ -673,8 +684,14 @@ export function apply(ctx, config) {
|
|
|
673
684
|
});
|
|
674
685
|
}
|
|
675
686
|
|
|
676
|
-
// Heat counting (L4 feedback).
|
|
677
|
-
|
|
687
|
+
// Heat counting (L4 feedback). v0.2.1: upgrade to { n, ts } so the
|
|
688
|
+
// hot gate can decay stale heat; legacy number entries are kept
|
|
689
|
+
// readable by isHotTool (ts=0 → outside any positive window).
|
|
690
|
+
const prev = feedback.calls[name];
|
|
691
|
+
feedback.calls[name] = {
|
|
692
|
+
n: (typeof prev === "number" ? prev : prev?.n || 0) + 1,
|
|
693
|
+
ts: Date.now(),
|
|
694
|
+
};
|
|
678
695
|
|
|
679
696
|
// Firewall: record the call sequence and flag exfil chains.
|
|
680
697
|
if (cfg.firewallEnabled !== false) {
|
package/lib/metrics.js
CHANGED
|
@@ -59,3 +59,68 @@ export function selectionCoverage(visibleNames, calledNames) {
|
|
|
59
59
|
uncovered,
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Hot-promotion gate with sliding-window decay (v0.2.1).
|
|
65
|
+
* feedback.calls entries may be:
|
|
66
|
+
* - new shape { n, ts } — n calls, ts = last call epoch ms;
|
|
67
|
+
* - legacy shape (number) — pre-v0.2.1 counts with no timestamp.
|
|
68
|
+
* A tool is hot when n >= hotThreshold AND (windowMs <= 0 OR the last call
|
|
69
|
+
* is inside the window). Legacy numbers carry ts=0 → outside any positive
|
|
70
|
+
* window → not hot (a tool that hasn't been called since the upgrade
|
|
71
|
+
* naturally loses its historical heat — this is the decay the old code
|
|
72
|
+
* lacked). windowMs=0 keeps the legacy forever-hot behavior for opt-out.
|
|
73
|
+
* @param {object|null} calls - feedback.calls map.
|
|
74
|
+
* @param {string} name - tool name.
|
|
75
|
+
* @param {number} hotThreshold - min calls to be hot.
|
|
76
|
+
* @param {number} windowMs - decay window; <=0 disables time checks.
|
|
77
|
+
* @returns {boolean}
|
|
78
|
+
*/
|
|
79
|
+
export function isHotTool(calls, name, hotThreshold, windowMs) {
|
|
80
|
+
if (!calls || typeof calls !== "object") return false;
|
|
81
|
+
const v = calls[name];
|
|
82
|
+
if (v == null) return false;
|
|
83
|
+
const n = typeof v === "number" ? v : v.n;
|
|
84
|
+
const ts = typeof v === "number" ? 0 : v.ts;
|
|
85
|
+
if (!(n >= hotThreshold)) return false;
|
|
86
|
+
if (windowMs > 0 && !(ts > 0 && Date.now() - ts <= windowMs)) return false;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Deterministic retrieval priority (v0.2.1, borrowed from Hermes-style
|
|
92
|
+
* "exact name > name token > description" fallback chains): the query's
|
|
93
|
+
* exact tool-name match always ranks first; a ≥4-char query that is a
|
|
94
|
+
* substring of a tool name is pulled ahead of BM25/semantic hits. Everything
|
|
95
|
+
* else keeps the given hit order, deduplicated.
|
|
96
|
+
* @param {Array<{name: string}>} docs - the tool corpus (allTools).
|
|
97
|
+
* @param {string} q - the search query.
|
|
98
|
+
* @param {Array<{id: string, score: number}>} hits - ranked hits.
|
|
99
|
+
* @returns {Array<{id: string, score: number}>} re-ordered hits.
|
|
100
|
+
*/
|
|
101
|
+
export function prioritizeExact(docs, q, hits) {
|
|
102
|
+
const query = String(q || "").trim().toLowerCase();
|
|
103
|
+
if (!query || !Array.isArray(docs) || !Array.isArray(hits)) return hits;
|
|
104
|
+
const ordered = [];
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
const exact = docs.find((d) => String(d.name || "").toLowerCase() === query);
|
|
107
|
+
if (exact) {
|
|
108
|
+
ordered.push({ id: exact.name, score: 0 });
|
|
109
|
+
seen.add(exact.name);
|
|
110
|
+
}
|
|
111
|
+
if (query.length >= 4) {
|
|
112
|
+
for (const d of docs) {
|
|
113
|
+
if (seen.has(d.name)) continue;
|
|
114
|
+
if (String(d.name || "").toLowerCase().includes(query)) {
|
|
115
|
+
ordered.push({ id: d.name, score: 0 });
|
|
116
|
+
seen.add(d.name);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const h of hits) {
|
|
121
|
+
if (!h || seen.has(h.id)) continue;
|
|
122
|
+
ordered.push(h);
|
|
123
|
+
seen.add(h.id);
|
|
124
|
+
}
|
|
125
|
+
return ordered;
|
|
126
|
+
}
|
package/package.json
CHANGED
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-tool-folder",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Fold the DSH tool surface per request + ChainGuard firewall (high-risk block + exfil-chain detection + anti-obfuscation) + BM25/bge-m3 hybrid tools_search. Shrinks schema tokens 80-90% while keeping selection accuracy. v0.2.0 adds a semantic retrieval leg (local Ollama bge-m3, RRF hybrid) and ChainGuard obfuscation detection (concat-rebuild / -enc / certutil / bitsadmin / homoglyphs).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dsh-plugin",
|
|
7
|
+
"dsh",
|
|
8
|
+
"deepseek-harness",
|
|
9
|
+
"cordis",
|
|
10
|
+
"tool-selection",
|
|
11
|
+
"token-optimization",
|
|
12
|
+
"bm25",
|
|
13
|
+
"semantic-search",
|
|
14
|
+
"security",
|
|
15
|
+
"firewall",
|
|
16
|
+
"obfuscation"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "lib/index.js",
|
|
20
|
+
"files": [
|
|
21
|
+
"lib",
|
|
22
|
+
"cordis.patch.yml",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"dsh": {
|
|
26
|
+
"bundle": {
|
|
27
|
+
"patch": "./cordis.patch.yml"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": "\u003e=18"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
39
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"test": "node --test \"test/*.test.js\""
|
|
43
|
+
},
|
|
44
|
+
"exports": {
|
|
45
|
+
".": "./lib/index.js",
|
|
46
|
+
"./lib/*": "./lib/*"
|
|
47
|
+
}
|
|
48
|
+
}
|