dsh-agentic-tool-catalog 0.0.0-stage → 0.3.2

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.
@@ -0,0 +1,57 @@
1
+ <svg width="760" height="400" viewBox="0 0 760 400" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="760" height="400" rx="20" fill="#F5F7FB"/>
3
+
4
+ <!-- left: product surface -->
5
+ <rect x="24" y="32" width="318" height="336" rx="16" fill="#FFFFFF" stroke="#E3E8EF"/>
6
+ <text x="44" y="62" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="#8A93A0">In context</text>
7
+
8
+ <!-- catalog panel -->
9
+ <rect x="42" y="78" width="282" height="230" rx="12" fill="#FFFFFF" stroke="#E3E8EF"/>
10
+ <text x="58" y="104" font-family="system-ui, -apple-system, sans-serif" font-size="13" font-weight="600" fill="#1F2329">Catalog</text>
11
+ <circle cx="270" cy="99" r="4" fill="#2FBF71"/>
12
+ <text x="280" y="104" font-family="system-ui, -apple-system, sans-serif" font-size="11" fill="#6B7280">ready</text>
13
+
14
+ <rect x="58" y="118" width="250" height="6" rx="3" fill="#E9EDF2"/>
15
+ <rect x="58" y="118" width="80" height="6" rx="3" fill="#9AA6B5"/>
16
+ <rect x="138" y="118" width="20" height="6" rx="3" fill="#145AF3"/>
17
+ <rect x="158" y="118" width="150" height="6" rx="3" fill="#D8DEE7"/>
18
+
19
+ <circle cx="61" cy="145" r="4" fill="#9AA6B5"/>
20
+ <text x="72" y="149" font-family="system-ui, -apple-system, sans-serif" font-size="11.5" fill="#6B7280">Always available</text>
21
+ <text x="292" y="149" text-anchor="end" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="#1F2329">32</text>
22
+
23
+ <circle cx="61" cy="169" r="4" fill="#145AF3"/>
24
+ <text x="72" y="173" font-family="system-ui, -apple-system, sans-serif" font-size="11.5" fill="#6B7280">Loaded</text>
25
+ <text x="292" y="173" text-anchor="end" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="#1F2329">8</text>
26
+
27
+ <circle cx="61" cy="193" r="4" fill="#D8DEE7"/>
28
+ <text x="72" y="197" font-family="system-ui, -apple-system, sans-serif" font-size="11.5" fill="#6B7280">On demand</text>
29
+ <text x="292" y="197" text-anchor="end" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="#1F2329">92</text>
30
+
31
+ <text x="58" y="222" font-family="system-ui, -apple-system, sans-serif" font-size="10.5" fill="#8A93A0">19 groups · catalog generated 2m ago</text>
32
+ <text x="58" y="252" font-family="system-ui, -apple-system, sans-serif" font-size="10.5" fill="#8A93A0">Only this small directory stays in context.</text>
33
+
34
+
35
+ <!-- right: headline + metrics -->
36
+ <text x="376" y="58" font-family="system-ui, -apple-system, sans-serif" font-size="20" font-weight="700" fill="#1F2329">Tool schemas stay out of context.</text>
37
+ <text x="376" y="82" font-family="system-ui, -apple-system, sans-serif" font-size="12.5" fill="#4B5563">An agent reads your tools, builds a catalog,</text>
38
+ <text x="376" y="98" font-family="system-ui, -apple-system, sans-serif" font-size="12.5" fill="#4B5563">and loads tools on demand.</text>
39
+
40
+ <rect x="376" y="112" width="344" height="72" rx="12" fill="#FFFFFF" stroke="#E3E8EF"/>
41
+ <text x="394" y="140" font-family="system-ui, -apple-system, sans-serif" font-size="24" font-weight="700" fill="#145AF3">−83%</text>
42
+ <text x="474" y="134" font-family="system-ui, -apple-system, sans-serif" font-size="11.5" fill="#6B7280">tool-schema payload</text>
43
+ <text x="474" y="151" font-family="system-ui, -apple-system, sans-serif" font-size="11.5" fill="#1F2329">131,390 → 21,877 chars</text>
44
+
45
+ <rect x="376" y="194" width="344" height="72" rx="12" fill="#FFFFFF" stroke="#E3E8EF"/>
46
+ <text x="394" y="222" font-family="system-ui, -apple-system, sans-serif" font-size="24" font-weight="700" fill="#145AF3">−62.6%</text>
47
+ <text x="498" y="216" font-family="system-ui, -apple-system, sans-serif" font-size="11.5" fill="#6B7280">first-request prompt tokens</text>
48
+ <text x="498" y="233" font-family="system-ui, -apple-system, sans-serif" font-size="11.5" fill="#1F2329">49,504 → 18,536 (real usage)</text>
49
+
50
+ <rect x="376" y="276" width="344" height="72" rx="12" fill="#FFFFFF" stroke="#E3E8EF"/>
51
+ <text x="394" y="304" font-family="system-ui, -apple-system, sans-serif" font-size="24" font-weight="700" fill="#145AF3">100%</text>
52
+ <text x="474" y="298" font-family="system-ui, -apple-system, sans-serif" font-size="11.5" fill="#6B7280">task success, both arms</text>
53
+ <text x="474" y="315" font-family="system-ui, -apple-system, sans-serif" font-size="11.5" fill="#1F2329">10/10 · 5 tasks × 2 runs</text>
54
+
55
+ <text x="376" y="370" font-family="system-ui, -apple-system, sans-serif" font-size="10.5" fill="#8A93A0">Measured on DSH 0.2.2-rc.1 · deepseek-flash</text>
56
+ <text x="376" y="384" font-family="system-ui, -apple-system, sans-serif" font-size="10.5" fill="#8A93A0">Same tasks, model and tool surface.</text>
57
+ </svg>
package/docs/detail.md ADDED
@@ -0,0 +1,33 @@
1
+ ![Agentic Tool Catalog: an agent builds the tool catalog, schemas stay out of context](./detail-hero.svg)
2
+
3
+ **All your tools. None of the context cost.**
4
+
5
+ Agentic Tool Catalog keeps a small, agent-built directory in context and loads tool schemas
6
+ only when a task needs them.
7
+
8
+ DSH normally sends every visible tool schema with every request. This plugin has a DSH
9
+ sub-agent organize your tools into a compact catalog — then only that catalog stays in
10
+ context, and `search_tools` loads the right tools on demand.
11
+
12
+ ## What you get
13
+
14
+ - **A much smaller context** — in our A/B test the tool-schema payload dropped **83.3%** and
15
+ the real first-request prompt dropped **62.6%** (about 31k fewer tokens per request).
16
+ - **No setup** — install a plugin or MCP server and the catalog regenerates when tools change.
17
+ - **Works with any tool** — plugin, MCP, or built-in; other plugins don't need to cooperate.
18
+ - **Local and private** — generated by your own model, cached in official DSH Storage.
19
+
20
+ ![With vs without: schema payload −83.3%, first-request prompt tokens −62.6%, total tokens −40.6%](./detail-benchmark.svg)
21
+
22
+ Same tasks, same model, same tool surface; task success was 100% both with and without the
23
+ plugin. The catalog saves context, not by making tasks cheaper to run.
24
+
25
+ ## Good to know
26
+
27
+ - Works with DSH `>=0.2.1-rc.2 <0.3.0`; tested on `0.2.2-rc.1` and `0.2.2-rc.2`.
28
+ - v1 covers **tools**; native skills keep their normal behavior.
29
+ - The catalog is derived data and contains no messages, prompts, tool results or credentials.
30
+ - It does **not** regenerate on a timer: only when the set of available tools changes. One-time
31
+ generation is cached, so a stable tool set costs nothing extra after the first run.
32
+ - If a refresh fails, the last successful catalog stays active and retries; it never silently
33
+ hides tools.
@@ -0,0 +1,30 @@
1
+ ![Agentic Tool Catalog:由 Agent 生成并维护工具目录,schema 不占上下文](./detail-hero.svg)
2
+
3
+ **全部工具都在,但不占上下文成本。**
4
+
5
+ Agent 工具目录在上下文里只保留一份由 Agent 生成的小目录;工具 schema 等到任务真正需要时才加载。
6
+
7
+ DSH 默认会把每个可见工具的完整 schema 放进每一次请求。这个插件让 DSH 子代理先把工具整理成
8
+ 一份紧凑目录——之后上下文里只有目录,`search_tools` 在需要时加载对应工具。
9
+
10
+ ## 它能给你什么
11
+
12
+ - **上下文小得多**——在我们的 A/B 实测中,工具 schema 载荷下降 **83.3%**,真实首请求 prompt
13
+ 下降 **62.6%**(每请求约少 3.1 万 tokens)。
14
+ - **无需配置**——新装插件或 MCP server 后,工具变化时目录会自动重建。
15
+ - **任何工具都能用**——插件、MCP、内置工具;其他插件不需要配合。
16
+ - **本地、私密**——由本机模型生成,缓存进官方 DSH Storage。
17
+
18
+ ![用 vs 不用:schema 载荷 −83.3%、首请求 prompt −62.6%、总 tokens −40.6%](./detail-benchmark.svg)
19
+
20
+ 同一批任务、同一模型、同一工具面;用与不用这个插件,任务成功率都是 100%。它省的是上下文,
21
+ 不是把任务本身变便宜。
22
+
23
+ ## 你需要知道
24
+
25
+ - 适用于 DSH `>=0.2.1-rc.2 <0.3.0`;已在 `0.2.2-rc.1` 与 `0.2.2-rc.2` 上验证。
26
+ - v1 覆盖 **tools**;DSH 原生 skills 行为不变。
27
+ - 目录是派生数据,不含消息、提示词、工具结果或凭据。
28
+ - **不会定时重新生成**:只有可用工具集发生变化时才重建。生成结果会被缓存,工具集稳定的情况下
29
+ 首次生成之后没有额外成本。
30
+ - 如果刷新失败,会继续使用上一次成功的目录并自动重试,不会静默隐藏工具。
package/icon.svg ADDED
@@ -0,0 +1,15 @@
1
+ <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect x="5" y="6" width="19.5" height="24" rx="3.6" stroke="url(#dsh-atc-card)" stroke-width="3.2" />
3
+ <path d="M10.5 13.5h8.2M10.5 18h8.2M10.5 22.5h5.2" stroke="url(#dsh-atc-lines)" stroke-width="3.2" stroke-linecap="round" />
4
+ <path d="M27.6 4.8l1.15 2.95 2.95 1.15-2.95 1.15-1.15 2.95-1.15-2.95L23.5 8.9l2.95-1.15z" fill="#45D9E7" />
5
+ <defs>
6
+ <linearGradient id="dsh-atc-card" x1="5" y1="6" x2="24.5" y2="30" gradientUnits="userSpaceOnUse">
7
+ <stop stop-color="#7CB7FF" />
8
+ <stop offset="1" stop-color="#145AF3" />
9
+ </linearGradient>
10
+ <linearGradient id="dsh-atc-lines" x1="10.5" y1="13.5" x2="18.7" y2="22.5" gradientUnits="userSpaceOnUse">
11
+ <stop stop-color="#7CB7FF" />
12
+ <stop offset="1" stop-color="#145AF3" />
13
+ </linearGradient>
14
+ </defs>
15
+ </svg>
package/lib/catalog.js ADDED
@@ -0,0 +1,208 @@
1
+ /**
2
+ * policy.js —— Tool Discovery 的非策略底座(2026-11-10 用户决策:只保留真 agent 策略)。
3
+ *
4
+ * legacy/rules/bm25/oracle/minimal/Jev/LLM/full-combo 等旧策略已全部移除。
5
+ * 保留:常驻判定、工具名规范化、引导目录(bootstrap)、确定性搜索、note/摘要。
6
+ * 真正的分组由 `agent-groups.js` 的真子代理生成;生成完成前用 bootstrap 扁平目录过渡。
7
+ *
8
+ * @module @local/dsh-tool-discovery/policy
9
+ */
10
+ export const DEFAULT_RESIDENT = [
11
+ // 执行基本盘
12
+ 'bash', 'read', 'write', 'edit', 'glob', 'grep', 'read_image',
13
+ 'ask_user_question', 'todo_write', 'skill', 'job_*', 'web_search', 'web_fetch',
14
+ 'working_directory', 'load_workspace_dependencies', 'exit_plan_mode', 'present',
15
+ // 委派 / 通信
16
+ 'subagent', 'subagent_fork', 'send_message', 'list_agents', 'interrupt_agent',
17
+ // MCP 资源读取
18
+ 'list_mcp_resources', 'list_mcp_resource_templates', 'read_mcp_resource',
19
+ // discovery 自己的入口
20
+ 'search_tools', 'catalog_status',
21
+ // Agent Teams(own-layer,实际也不可 restrict;显式列出表达"就是常驻")
22
+ 'spawn_teammate', 'wait_agent', 'team_task_*',
23
+ ]
24
+
25
+ export function compileGlob(pattern) {
26
+ const escaped = String(pattern).replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')
27
+ return new RegExp(`^${escaped}$`)
28
+ }
29
+
30
+ export function tokensOf(name) {
31
+ return String(name).toLowerCase().split(/[^a-z0-9]+/).filter(Boolean)
32
+ }
33
+
34
+ export function normalizeGroupId(raw) {
35
+ const id = String(raw ?? '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64)
36
+ return id.length > 0 ? id : 'extensions'
37
+ }
38
+
39
+ export function mcpInfo(name) {
40
+ if (!name.startsWith('mcp__')) return undefined
41
+ const rest = name.slice('mcp__'.length)
42
+ const separator = rest.indexOf('__')
43
+ if (separator <= 0) return undefined
44
+ const server = rest.slice(0, separator)
45
+ return { server, id: `mcp-${normalizeGroupId(server)}` }
46
+ }
47
+
48
+ export function residentMatcher(config = {}) {
49
+ const override = Array.isArray(config.resident) && config.resident.length > 0 ? config.resident : undefined
50
+ const base = override ?? DEFAULT_RESIDENT
51
+ const patterns = [...base, ...(config.extraKeepResident ?? [])].map(compileGlob)
52
+ const deferred = (config.extraDeferred ?? []).map(compileGlob)
53
+ return (name) => {
54
+ if (deferred.some((re) => re.test(name))) return false
55
+ return patterns.some((re) => re.test(name))
56
+ }
57
+ }
58
+
59
+ function membersDescription(id, members, limit = 200) {
60
+ const names = members.map((m) => m.name)
61
+ const sample = names.slice(0, 8).join(', ')
62
+ const more = names.length > 8 ? ` (+${names.length - 8} more)` : ''
63
+ const prefix = id.startsWith('mcp-') ? `MCP server "${id.slice('mcp-'.length)}" tools: ` : 'Tools: '
64
+ const text = `${prefix}${sample}${more}.`
65
+ return text.length <= limit ? text : `${text.slice(0, limit - 1)}…`
66
+ }
67
+
68
+ export function summarizeGroups(entries, descriptions = new Map()) {
69
+ const byGroup = new Map()
70
+ for (const entry of entries) {
71
+ if (!byGroup.has(entry.group)) byGroup.set(entry.group, [])
72
+ byGroup.get(entry.group).push(entry)
73
+ }
74
+ return [...byGroup.entries()]
75
+ .map(([id, members]) => ({
76
+ id,
77
+ description: descriptions.get(id) ?? membersDescription(id, members),
78
+ count: members.length,
79
+ members: members.map((m) => m.name).sort(),
80
+ }))
81
+ .sort((a, b) => b.count - a.count || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
82
+ }
83
+
84
+ /** 引导目录:agent 分组尚未就绪时的最小目录(不假装是分组策略)。 */
85
+ export function buildBootstrapCatalog(tools) {
86
+ const entries = tools.map((tool) => ({ ...tool, group: 'others' }))
87
+ return {
88
+ strategy: 'agentic-tool-catalog',
89
+ source: 'bootstrap',
90
+ entries,
91
+ groups: [],
92
+ others: entries.map((entry) => entry.name).sort(),
93
+ warnings: [{ kind: 'catalog-bootstrap', message: `agent 分组尚未就绪:${tools.length} 个工具暂以扁平目录提供` }],
94
+ }
95
+ }
96
+
97
+ export const SCORE = { name: 6, description: 2, groupId: 4, groupDescription: 2 }
98
+
99
+ export const QUERY_STOPWORDS = new Set([
100
+ 'get', 'set', 'list', 'create', 'update', 'delete', 'remove', 'read', 'write',
101
+ 'add', 'edit', 'run', 'query', 'status', 'manage', 'execute', 'copy', 'move',
102
+ ])
103
+
104
+ export function singularize(token) {
105
+ const t = String(token).toLowerCase()
106
+ if (t.length <= 3) return t
107
+ if (t.endsWith('ies')) return `${t.slice(0, -3)}y`
108
+ if (t.endsWith('es')) return t.slice(0, -2)
109
+ if (t.endsWith('s')) return t.slice(0, -1)
110
+ return t
111
+ }
112
+
113
+ export function queryTokens(query) {
114
+ const raw = String(query ?? '').toLowerCase().split(/[^a-z0-9]+/).filter(Boolean)
115
+ const meaningful = raw.filter((t) => !QUERY_STOPWORDS.has(t) && !QUERY_STOPWORDS.has(singularize(t)))
116
+ const base = meaningful.length > 0 ? meaningful : raw
117
+ const out = new Set()
118
+ for (const token of base) {
119
+ out.add(token)
120
+ out.add(singularize(token))
121
+ }
122
+ return [...out]
123
+ }
124
+
125
+ export function rankTools(catalog, query, options = {}) {
126
+ const limit = Math.max(1, Math.min(20, options.limit ?? 6))
127
+ const groupMax = Math.max(1, options.groupMaxResults ?? 30)
128
+ const loadedSet = options.loaded instanceof Set ? options.loaded : new Set(options.loaded ?? [])
129
+ const raw = String(query ?? '').trim().toLowerCase()
130
+ const groupById = new Map(catalog.groups.map((g) => [g.id.toLowerCase(), g]))
131
+ const exact = groupById.get(raw)
132
+ if (exact !== undefined) {
133
+ const members = exact.members.filter((name) => !loadedSet.has(name))
134
+ const loaded = members.slice(0, groupMax)
135
+ return { loaded, more: 0, groupLoad: true, groupId: exact.id, remainingInGroup: members.length - loaded.length }
136
+ }
137
+ const tokens = queryTokens(raw)
138
+ if (tokens.length === 0) return { loaded: [], more: 0, groupLoad: false, groupId: null, remainingInGroup: 0 }
139
+ const groupText = new Map(catalog.groups.map((g) => [g.id, `${g.id} ${g.description}`.toLowerCase()]))
140
+ const scored = []
141
+ for (const entry of catalog.entries) {
142
+ if (loadedSet.has(entry.name)) continue
143
+ const name = entry.name.toLowerCase()
144
+ const description = String(entry.description ?? '').toLowerCase()
145
+ const gid = entry.group.toLowerCase()
146
+ const gtext = groupText.get(entry.group) ?? ''
147
+ let score = 0
148
+ for (const token of tokens) {
149
+ if (name.includes(token)) score += SCORE.name
150
+ if (description.includes(token)) score += SCORE.description
151
+ if (gid.includes(token)) score += SCORE.groupId
152
+ if (gtext.includes(token)) score += SCORE.groupDescription
153
+ }
154
+ if (score > 0) scored.push({ entry, score })
155
+ }
156
+ scored.sort((a, b) => b.score - a.score || (a.entry.name < b.entry.name ? -1 : a.entry.name > b.entry.name ? 1 : 0))
157
+ const picked = scored.slice(0, limit)
158
+ return {
159
+ loaded: picked.map((p) => p.entry.name),
160
+ more: scored.length - picked.length,
161
+ groupLoad: false,
162
+ groupId: null,
163
+ remainingInGroup: 0,
164
+ }
165
+ }
166
+
167
+ /** 系统提示 note:组 + 计数;rules 策略附带 others 名字(上限 maxOtherNames)。 */
168
+
169
+ export function noteText(catalog, loaded, options = {}) {
170
+ const maxOtherNames = options.maxOtherNames ?? 20
171
+ const loadedSet = loaded instanceof Set ? loaded : new Set(loaded ?? [])
172
+ const groupCounts = new Map()
173
+ for (const entry of catalog.entries) {
174
+ if (loadedSet.has(entry.name)) continue
175
+ if (entry.group === 'others') continue
176
+ groupCounts.set(entry.group, (groupCounts.get(entry.group) ?? 0) + 1)
177
+ }
178
+ const groups = [...groupCounts.entries()]
179
+ .map(([id, count]) => ({ id, count }))
180
+ .sort((a, b) => b.count - a.count || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
181
+ const others = catalog.others.filter((name) => !loadedSet.has(name))
182
+ if (groups.length === 0 && others.length === 0) return ''
183
+ const groupText = groups.map((g) => `${g.id} (${g.count})`).join(', ')
184
+ const shownOthers = others.slice(0, maxOtherNames)
185
+ const moreOthers = others.length - shownOthers.length
186
+ const othersText = shownOthers.length === 0
187
+ ? ''
188
+ : ` Others: ${shownOthers.join(', ')}${moreOthers > 0 ? ` (+${moreOthers} more)` : ''}.`
189
+ const head = groups.length > 0 ? `Some capability tools are deferred and not loaded: ${groupText}.` : 'Some capability tools are deferred and not loaded.'
190
+ const prefix = catalog.source === 'bootstrap'
191
+ ? `Tool catalog is initializing: ${catalog.entries.length} capabilities are searchable by name; grouping completes in the background. `
192
+ : catalog.source === 'agent-stale'
193
+ ? 'Note: using the most recent successful grouping while the catalog refreshes. '
194
+ : ''
195
+ return `${prefix}${head}${othersText} Use search_tools to load the ones you need before calling them.`
196
+ }
197
+
198
+ /** 零命中时的目录页:组 + 描述;others 单列。 */
199
+
200
+ export function catalogSummary(catalog, limit = 200) {
201
+ const lines = catalog.groups.map((g) => `- ${g.id} (${g.count} tools): ${String(g.description).slice(0, limit)}`)
202
+ if (catalog.others.length > 0) {
203
+ const sample = catalog.others.slice(0, 30).join(', ')
204
+ const more = catalog.others.length > 30 ? ` (+${catalog.others.length - 30} more)` : ''
205
+ lines.push(`- others (${catalog.others.length} tools): ${sample}${more}`)
206
+ }
207
+ return lines.join('\n')
208
+ }