skyloom 1.11.0 → 1.13.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/.github/workflows/ci.yml +36 -36
- package/README.md +142 -46
- package/config/default.yaml +43 -47
- package/config/models.yaml +155 -0
- package/config/providers.yaml +39 -39
- package/config/skills/api_integrator/SKILL.md +15 -15
- package/config/skills/arch_designer/SKILL.md +13 -13
- package/config/skills/ci_cd_manager/SKILL.md +14 -14
- package/config/skills/code_analysis/SKILL.md +13 -13
- package/config/skills/code_generator/SKILL.md +12 -12
- package/config/skills/code_reviewer/SKILL.md +13 -13
- package/config/skills/content_writer/SKILL.md +14 -14
- package/config/skills/data_transformer/SKILL.md +15 -15
- package/config/skills/document_analysis/SKILL.md +13 -13
- package/config/skills/emotional_companion/SKILL.md +15 -15
- package/config/skills/performance_checker/SKILL.md +14 -14
- package/config/skills/security_auditor/SKILL.md +14 -14
- package/config/skills/self_evolve/SKILL.md +13 -13
- package/config/skills/sys_operator/SKILL.md +15 -15
- package/config/skills/task_planner/SKILL.md +14 -14
- package/config/skills/web_research/SKILL.md +14 -14
- package/config/skills/workflow_designer/SKILL.md +13 -13
- package/dist/agents/dew.js +52 -52
- package/dist/agents/fair.js +84 -84
- package/dist/agents/fog.js +30 -30
- package/dist/agents/frost.js +32 -32
- package/dist/agents/rain.js +32 -32
- package/dist/agents/snow.js +68 -68
- package/dist/cli/main.js +172 -47
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +9 -1
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/agent/task.d.ts +58 -0
- package/dist/core/agent/task.d.ts.map +1 -0
- package/dist/core/agent/task.js +83 -0
- package/dist/core/agent/task.js.map +1 -0
- package/dist/core/agent.d.ts +2 -45
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +61 -145
- package/dist/core/agent.js.map +1 -1
- package/dist/core/agent_helpers.d.ts +10 -0
- package/dist/core/agent_helpers.d.ts.map +1 -1
- package/dist/core/agent_helpers.js +39 -0
- package/dist/core/agent_helpers.js.map +1 -1
- package/dist/core/catalog.d.ts +71 -0
- package/dist/core/catalog.d.ts.map +1 -0
- package/dist/core/catalog.js +176 -0
- package/dist/core/catalog.js.map +1 -0
- package/dist/core/config.d.ts +8 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +12 -4
- package/dist/core/config.js.map +1 -1
- package/dist/core/factory.js +16 -16
- package/dist/core/llm.d.ts +7 -0
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +139 -7
- package/dist/core/llm.js.map +1 -1
- package/dist/core/longdoc.js +5 -5
- package/dist/core/memory.d.ts.map +1 -1
- package/dist/core/memory.js +69 -62
- package/dist/core/memory.js.map +1 -1
- package/dist/core/theme.d.ts +46 -0
- package/dist/core/theme.d.ts.map +1 -0
- package/dist/core/theme.js +42 -0
- package/dist/core/theme.js.map +1 -0
- package/dist/web/server.js +542 -519
- package/dist/web/server.js.map +1 -1
- package/docs/AESTHETIC_DESIGN.md +144 -0
- package/docs/OPTIMIZATION_PLAN.md +178 -0
- package/package.json +60 -60
- package/scripts/install.js +48 -48
- package/scripts/link.js +10 -10
- package/setup.bat +79 -79
- package/skill-test-ty2fOA/test.md +10 -10
- package/src/agents/dew.ts +70 -70
- package/src/agents/fair.ts +102 -102
- package/src/agents/fog.ts +48 -48
- package/src/agents/frost.ts +50 -50
- package/src/agents/rain.ts +50 -50
- package/src/agents/snow.ts +239 -239
- package/src/cli/main.ts +425 -316
- package/src/cli/mode.ts +58 -58
- package/src/cli/tui.ts +272 -268
- package/src/core/agent/task.ts +100 -0
- package/src/core/agent.ts +1446 -1549
- package/src/core/agent_helpers.ts +496 -461
- package/src/core/arbitrate.ts +162 -162
- package/src/core/catalog.ts +178 -0
- package/src/core/checkpoint.ts +94 -94
- package/src/core/config.ts +20 -4
- package/src/core/estimate.ts +104 -104
- package/src/core/evolve.ts +191 -191
- package/src/core/factory.ts +627 -627
- package/src/core/filter.ts +103 -103
- package/src/core/graph.ts +156 -156
- package/src/core/icons.ts +53 -53
- package/src/core/index.ts +37 -37
- package/src/core/learn.ts +146 -146
- package/src/core/llm.ts +108 -5
- package/src/core/longdoc.ts +155 -155
- package/src/core/mcp_server.ts +176 -176
- package/src/core/memory.ts +1178 -1171
- package/src/core/profile.ts +255 -255
- package/src/core/router.ts +124 -124
- package/src/core/sandbox.ts +142 -142
- package/src/core/security.ts +243 -243
- package/src/core/skill.ts +342 -342
- package/src/core/theme.ts +65 -0
- package/src/core/tool_router.ts +193 -193
- package/src/core/vector.ts +152 -152
- package/src/core/workspace.ts +150 -150
- package/src/plugins/loader.ts +66 -66
- package/src/skills/loader.ts +46 -46
- package/src/sql.js.d.ts +29 -29
- package/src/tools/builtin.ts +380 -380
- package/src/tools/computer.ts +269 -269
- package/src/tools/delegate.ts +49 -49
- package/src/web/server.ts +660 -634
- package/src/web/tts.ts +93 -93
- package/tests/agent_helpers.test.ts +48 -0
- package/tests/bus.test.ts +121 -121
- package/tests/catalog.test.ts +86 -0
- package/tests/config.test.ts +41 -0
- package/tests/icons.test.ts +45 -45
- package/tests/memory.test.ts +147 -0
- package/tests/router.test.ts +86 -86
- package/tests/schemas.test.ts +51 -51
- package/tests/semantic.test.ts +83 -83
- package/tests/setup.ts +10 -10
- package/tests/skill.test.ts +172 -172
- package/tests/task.test.ts +60 -0
- package/tests/tool.test.ts +108 -108
- package/tests/tool_router.test.ts +71 -71
- package/vitest.config.ts +17 -17
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design tokens — single source of truth for Skyloom's "水墨气象台" identity.
|
|
3
|
+
*
|
|
4
|
+
* One definition drives every surface: CLI (chalk truecolor), the full-screen
|
|
5
|
+
* TUI, and the Web ink-wash UI. Change a pigment here and all three follow.
|
|
6
|
+
*
|
|
7
|
+
* Design rationale: docs/AESTHETIC_DESIGN.md
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Base paper-and-ink palette (hex). */
|
|
11
|
+
export const PALETTE = {
|
|
12
|
+
paper: "#f8f4ec",
|
|
13
|
+
paperWarm: "#f3ede2",
|
|
14
|
+
inkDeep: "#1a1614",
|
|
15
|
+
inkMid: "#3d3833",
|
|
16
|
+
inkLight: "#8c8680",
|
|
17
|
+
inkFaint: "#c4bfb8",
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
/** Per-agent identity: weather + mineral pigment + classical poem + motion. */
|
|
21
|
+
export interface AgentTheme {
|
|
22
|
+
/** Agent key (fog/rain/…). */
|
|
23
|
+
name: string;
|
|
24
|
+
/** Weather kanji used as a seal stamp (霧/雨/…). */
|
|
25
|
+
kanji: string;
|
|
26
|
+
/** Single-glyph weather symbol used across CLI/TUI/Web. */
|
|
27
|
+
symbol: string;
|
|
28
|
+
/** Mineral pigment hex. */
|
|
29
|
+
hex: string;
|
|
30
|
+
/** Mineral pigment RGB triple (for ANSI truecolor / CSS rgba). */
|
|
31
|
+
rgb: [number, number, number];
|
|
32
|
+
/** Pigment name in Chinese (松烟墨/石青/…). */
|
|
33
|
+
pigment: string;
|
|
34
|
+
/** Responsibility (探索洞察/创造产出/…). */
|
|
35
|
+
specialty: string;
|
|
36
|
+
/** Classical poem line shown in empty states / sidebars. */
|
|
37
|
+
poem: string;
|
|
38
|
+
/** Motion keyword (drift/fall/glint/float/bead/rise). */
|
|
39
|
+
motion: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function rgbOf(hex: string): [number, number, number] {
|
|
43
|
+
const h = hex.replace("#", "");
|
|
44
|
+
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const AGENT_THEMES: Record<string, AgentTheme> = {
|
|
48
|
+
fog: { name: "fog", kanji: "霧", symbol: "≋", hex: "#4a4a44", rgb: rgbOf("#4a4a44"), pigment: "松烟墨", specialty: "探索洞察", poem: "山色有无中", motion: "drift" },
|
|
49
|
+
rain: { name: "rain", kanji: "雨", symbol: "⸽", hex: "#2a5c8a", rgb: rgbOf("#2a5c8a"), pigment: "石青", specialty: "创造产出", poem: "一蓑烟雨任平生", motion: "fall" },
|
|
50
|
+
frost: { name: "frost", kanji: "霜", symbol: "✱", hex: "#3a7a6e", rgb: rgbOf("#3a7a6e"), pigment: "石绿", specialty: "精炼品质", poem: "月落乌啼霜满天", motion: "glint" },
|
|
51
|
+
snow: { name: "snow", kanji: "雪", symbol: "❉", hex: "#8a8a82", rgb: rgbOf("#8a8a82"), pigment: "铅白", specialty: "架构规划", poem: "千树万树梨花开", motion: "float" },
|
|
52
|
+
dew: { name: "dew", kanji: "露", symbol: "∘", hex: "#8b6914", rgb: rgbOf("#8b6914"), pigment: "赭石", specialty: "可靠守护", poem: "金风玉露一相逢", motion: "bead" },
|
|
53
|
+
fair: { name: "fair", kanji: "晴", symbol: "☼", hex: "#b3342d", rgb: rgbOf("#b3342d"), pigment: "朱砂", specialty: "情感陪伴", poem: "道是无晴却有晴", motion: "rise" },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Ordered agent keys (織機 six shuttles). */
|
|
57
|
+
export const AGENT_ORDER = ["fog", "rain", "frost", "snow", "dew", "fair"] as const;
|
|
58
|
+
|
|
59
|
+
/** Brand seal pigment (朱砂) — used for the active-agent stamp everywhere. */
|
|
60
|
+
export const SEAL_HEX = AGENT_THEMES.fair.hex;
|
|
61
|
+
|
|
62
|
+
/** Look up an agent theme, defaulting to fog. */
|
|
63
|
+
export function agentTheme(name: string): AgentTheme {
|
|
64
|
+
return AGENT_THEMES[name] ?? AGENT_THEMES.fog;
|
|
65
|
+
}
|
package/src/core/tool_router.ts
CHANGED
|
@@ -1,193 +1,193 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool-subset selection for LLM calls.
|
|
3
|
-
*
|
|
4
|
-
* Without filtering, every chat turn ships ~50 tool schemas (built-ins + MCP +
|
|
5
|
-
* skill-required + delegation) to the model. That dilutes attention (the LLM
|
|
6
|
-
* picks plausible-but-wrong tools more often) and burns 8-15k input tokens per
|
|
7
|
-
* turn. This module narrows the active tool set to ~12 by lightweight scoring
|
|
8
|
-
* against the user's latest message.
|
|
9
|
-
*
|
|
10
|
-
* The router intentionally avoids embeddings / LLM calls — it must run in <1ms
|
|
11
|
-
* on every turn, before the real LLM call. A coarse keyword/substring score is
|
|
12
|
-
* good enough to keep the right tools in and bad enough to be cheap.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { ToolDefinition, ToolRegistry } from './tool';
|
|
16
|
-
|
|
17
|
-
// Infrastructure tools that are useful on most turns.
|
|
18
|
-
const INFRA_TOOLS: ReadonlySet<string> = new Set([
|
|
19
|
-
'delegate_to',
|
|
20
|
-
'list_skills',
|
|
21
|
-
'use_skill',
|
|
22
|
-
'recall_facts',
|
|
23
|
-
'remember_fact',
|
|
24
|
-
]);
|
|
25
|
-
|
|
26
|
-
const MUTATING_TOOLS: ReadonlySet<string> = new Set([
|
|
27
|
-
'write_file',
|
|
28
|
-
'edit_file',
|
|
29
|
-
'move_file',
|
|
30
|
-
'copy_file',
|
|
31
|
-
'delete_file',
|
|
32
|
-
'git_add',
|
|
33
|
-
'git_commit',
|
|
34
|
-
'git_checkout',
|
|
35
|
-
'shell_exec',
|
|
36
|
-
'http_post',
|
|
37
|
-
'mcp_add_server',
|
|
38
|
-
'mcp_remove_server',
|
|
39
|
-
'mcp_scaffold_server',
|
|
40
|
-
'kill_process',
|
|
41
|
-
'package_manager',
|
|
42
|
-
'service_control',
|
|
43
|
-
]);
|
|
44
|
-
|
|
45
|
-
const TOKEN_RE = /[A-Za-z][A-Za-z0-9_]*|[一-鿿]+/g;
|
|
46
|
-
|
|
47
|
-
const STOPWORDS: ReadonlySet<string> = new Set([
|
|
48
|
-
'a', 'an', 'the', 'is', 'are', 'was', 'were', 'and', 'or', 'but',
|
|
49
|
-
'to', 'for', 'of', 'in', 'on', 'at', 'with', 'by', 'do', 'did',
|
|
50
|
-
'does', 'i', 'me', 'my', 'you', 'your', 'it', 'this', 'that',
|
|
51
|
-
'what', 'how', 'can', 'could', 'would', 'should', 'please',
|
|
52
|
-
'tell', 'show', 'help', 'ok', 'yes', 'no',
|
|
53
|
-
'好', '的', '是', '我', '你', '他', '她', '它', '这', '那',
|
|
54
|
-
'什么', '怎么', '请', '帮', '麻烦',
|
|
55
|
-
]);
|
|
56
|
-
|
|
57
|
-
function tokenize(text: string): Set<string> {
|
|
58
|
-
const tokens = new Set<string>();
|
|
59
|
-
const matches = text?.toLowerCase().match(TOKEN_RE) || [];
|
|
60
|
-
for (const t of matches) {
|
|
61
|
-
if (t.length >= 2 && !STOPWORDS.has(t)) {
|
|
62
|
-
tokens.add(t);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return tokens;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function scoreTool(tool: ToolDefinition, queryTokens: Set<string>, queryLc: string): number {
|
|
69
|
-
if (queryTokens.size === 0) return 0;
|
|
70
|
-
let score = 0;
|
|
71
|
-
|
|
72
|
-
// Tool name tokens carry the strongest signal
|
|
73
|
-
const nameTokens = tokenize(tool.name.replace(/_/g, ' '));
|
|
74
|
-
for (const qt of queryTokens) {
|
|
75
|
-
if (nameTokens.has(qt)) {
|
|
76
|
-
score += 5;
|
|
77
|
-
} else if (tool.name.toLowerCase().includes(qt)) {
|
|
78
|
-
score += 3;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Description tokens are weaker
|
|
83
|
-
const descTokens = tokenize(tool.description);
|
|
84
|
-
for (const qt of queryTokens) {
|
|
85
|
-
if (descTokens.has(qt)) {
|
|
86
|
-
score += 1;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Intent boosts for common Chinese/English tasks
|
|
91
|
-
const name = tool.name;
|
|
92
|
-
if (['read_file', 'list_directory', 'tree', 'file_search', 'code_search', 'grep'].includes(name) &&
|
|
93
|
-
['read', 'file', 'inspect', 'search', 'grep', '看', '读', '查', '搜索', '文件', '代码'].some(k => queryLc.includes(k))) {
|
|
94
|
-
score += 4;
|
|
95
|
-
}
|
|
96
|
-
if (['write_file', 'edit_file', 'move_file', 'copy_file', 'delete_file'].includes(name) &&
|
|
97
|
-
['write', 'edit', 'modify', 'fix', 'save', '生成', '写', '改', '修复', '保存', '删除'].some(k => queryLc.includes(k))) {
|
|
98
|
-
score += 4;
|
|
99
|
-
}
|
|
100
|
-
if (name.startsWith('git_') &&
|
|
101
|
-
['git', 'commit', 'diff', 'branch', '提交', '分支', '差异'].some(k => queryLc.includes(k))) {
|
|
102
|
-
score += 4;
|
|
103
|
-
}
|
|
104
|
-
if (['web_search', 'fetch_page', 'http_get'].includes(name) &&
|
|
105
|
-
['web', 'url', 'http', 'research', '搜索', '网页', '联网', '资料'].some(k => queryLc.includes(k))) {
|
|
106
|
-
score += 4;
|
|
107
|
-
}
|
|
108
|
-
if (['list_skills', 'use_skill'].includes(name) &&
|
|
109
|
-
['skill', '能力', '技能', 'ppt', 'pdf', 'excel', 'xlsx', 'docx'].some(k => queryLc.includes(k))) {
|
|
110
|
-
score += 4;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return score;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Return up to ~topK tool names ordered by relevance to the query.
|
|
118
|
-
*
|
|
119
|
-
* Always-included infrastructure tools and mustInclude (e.g. active
|
|
120
|
-
* skill required_tools) are appended regardless of score. When the candidate
|
|
121
|
-
* set is already small (<= topK + |mustInclude|), no filtering is applied.
|
|
122
|
-
*
|
|
123
|
-
* A short or empty query means we have no signal to filter — returning the
|
|
124
|
-
* full candidate set is correct in that case.
|
|
125
|
-
*/
|
|
126
|
-
export function selectRelevantTools(
|
|
127
|
-
registry: ToolRegistry,
|
|
128
|
-
candidateNames: string[],
|
|
129
|
-
query: string,
|
|
130
|
-
options?: {
|
|
131
|
-
topK?: number;
|
|
132
|
-
mustInclude?: Set<string>;
|
|
133
|
-
}
|
|
134
|
-
): string[] {
|
|
135
|
-
const topK = options?.topK ?? 12;
|
|
136
|
-
const explicitMust = new Set(options?.mustInclude ?? []);
|
|
137
|
-
|
|
138
|
-
const infraPresent = candidateNames.filter(n => INFRA_TOOLS.has(n) && !explicitMust.has(n));
|
|
139
|
-
const mustPresent = candidateNames.filter(n => explicitMust.has(n));
|
|
140
|
-
const remaining = candidateNames.filter(n => !explicitMust.has(n) && !INFRA_TOOLS.has(n));
|
|
141
|
-
|
|
142
|
-
const queryTokens = tokenize(query);
|
|
143
|
-
const queryLc = (query || '').toLowerCase();
|
|
144
|
-
const smallSurface = candidateNames.length <= topK + mustPresent.length + infraPresent.length;
|
|
145
|
-
const lowSignal = queryTokens.size < 2 && queryLc.length < 8;
|
|
146
|
-
|
|
147
|
-
// No filtering when the set is already small or the query is too short
|
|
148
|
-
if (smallSurface || lowSignal) {
|
|
149
|
-
return [...mustPresent, ...infraPresent, ...remaining];
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const scored: Array<{ name: string; score: number; penalty: number }> = [];
|
|
153
|
-
const allScoredNames = [...infraPresent, ...remaining];
|
|
154
|
-
|
|
155
|
-
for (const name of allScoredNames) {
|
|
156
|
-
const tool = registry.get(name);
|
|
157
|
-
if (!tool) continue;
|
|
158
|
-
const score = scoreTool(tool, queryTokens, queryLc);
|
|
159
|
-
const penalty = MUTATING_TOOLS.has(name) && score === 0 ? 1 : 0;
|
|
160
|
-
scored.push({ name, score, penalty });
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Stable sort by descending score; zero-score tools only fill spare slots
|
|
164
|
-
scored.sort((a, b) => {
|
|
165
|
-
if (a.score !== b.score) return b.score - a.score;
|
|
166
|
-
return a.penalty - b.penalty;
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
const budget = Math.max(0, topK - mustPresent.length);
|
|
170
|
-
const picked: string[] = [];
|
|
171
|
-
const pickedSet = new Set<string>();
|
|
172
|
-
|
|
173
|
-
for (const item of scored) {
|
|
174
|
-
if (picked.length >= budget) break;
|
|
175
|
-
if (item.score > 0) {
|
|
176
|
-
picked.push(item.name);
|
|
177
|
-
pickedSet.add(item.name);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Fill remaining slots with zero-score tools
|
|
182
|
-
if (picked.length < budget) {
|
|
183
|
-
for (const item of scored) {
|
|
184
|
-
if (picked.length >= budget) break;
|
|
185
|
-
if (!pickedSet.has(item.name)) {
|
|
186
|
-
picked.push(item.name);
|
|
187
|
-
pickedSet.add(item.name);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return [...mustPresent, ...picked];
|
|
193
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Tool-subset selection for LLM calls.
|
|
3
|
+
*
|
|
4
|
+
* Without filtering, every chat turn ships ~50 tool schemas (built-ins + MCP +
|
|
5
|
+
* skill-required + delegation) to the model. That dilutes attention (the LLM
|
|
6
|
+
* picks plausible-but-wrong tools more often) and burns 8-15k input tokens per
|
|
7
|
+
* turn. This module narrows the active tool set to ~12 by lightweight scoring
|
|
8
|
+
* against the user's latest message.
|
|
9
|
+
*
|
|
10
|
+
* The router intentionally avoids embeddings / LLM calls — it must run in <1ms
|
|
11
|
+
* on every turn, before the real LLM call. A coarse keyword/substring score is
|
|
12
|
+
* good enough to keep the right tools in and bad enough to be cheap.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ToolDefinition, ToolRegistry } from './tool';
|
|
16
|
+
|
|
17
|
+
// Infrastructure tools that are useful on most turns.
|
|
18
|
+
const INFRA_TOOLS: ReadonlySet<string> = new Set([
|
|
19
|
+
'delegate_to',
|
|
20
|
+
'list_skills',
|
|
21
|
+
'use_skill',
|
|
22
|
+
'recall_facts',
|
|
23
|
+
'remember_fact',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const MUTATING_TOOLS: ReadonlySet<string> = new Set([
|
|
27
|
+
'write_file',
|
|
28
|
+
'edit_file',
|
|
29
|
+
'move_file',
|
|
30
|
+
'copy_file',
|
|
31
|
+
'delete_file',
|
|
32
|
+
'git_add',
|
|
33
|
+
'git_commit',
|
|
34
|
+
'git_checkout',
|
|
35
|
+
'shell_exec',
|
|
36
|
+
'http_post',
|
|
37
|
+
'mcp_add_server',
|
|
38
|
+
'mcp_remove_server',
|
|
39
|
+
'mcp_scaffold_server',
|
|
40
|
+
'kill_process',
|
|
41
|
+
'package_manager',
|
|
42
|
+
'service_control',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const TOKEN_RE = /[A-Za-z][A-Za-z0-9_]*|[一-鿿]+/g;
|
|
46
|
+
|
|
47
|
+
const STOPWORDS: ReadonlySet<string> = new Set([
|
|
48
|
+
'a', 'an', 'the', 'is', 'are', 'was', 'were', 'and', 'or', 'but',
|
|
49
|
+
'to', 'for', 'of', 'in', 'on', 'at', 'with', 'by', 'do', 'did',
|
|
50
|
+
'does', 'i', 'me', 'my', 'you', 'your', 'it', 'this', 'that',
|
|
51
|
+
'what', 'how', 'can', 'could', 'would', 'should', 'please',
|
|
52
|
+
'tell', 'show', 'help', 'ok', 'yes', 'no',
|
|
53
|
+
'好', '的', '是', '我', '你', '他', '她', '它', '这', '那',
|
|
54
|
+
'什么', '怎么', '请', '帮', '麻烦',
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
function tokenize(text: string): Set<string> {
|
|
58
|
+
const tokens = new Set<string>();
|
|
59
|
+
const matches = text?.toLowerCase().match(TOKEN_RE) || [];
|
|
60
|
+
for (const t of matches) {
|
|
61
|
+
if (t.length >= 2 && !STOPWORDS.has(t)) {
|
|
62
|
+
tokens.add(t);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return tokens;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function scoreTool(tool: ToolDefinition, queryTokens: Set<string>, queryLc: string): number {
|
|
69
|
+
if (queryTokens.size === 0) return 0;
|
|
70
|
+
let score = 0;
|
|
71
|
+
|
|
72
|
+
// Tool name tokens carry the strongest signal
|
|
73
|
+
const nameTokens = tokenize(tool.name.replace(/_/g, ' '));
|
|
74
|
+
for (const qt of queryTokens) {
|
|
75
|
+
if (nameTokens.has(qt)) {
|
|
76
|
+
score += 5;
|
|
77
|
+
} else if (tool.name.toLowerCase().includes(qt)) {
|
|
78
|
+
score += 3;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Description tokens are weaker
|
|
83
|
+
const descTokens = tokenize(tool.description);
|
|
84
|
+
for (const qt of queryTokens) {
|
|
85
|
+
if (descTokens.has(qt)) {
|
|
86
|
+
score += 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Intent boosts for common Chinese/English tasks
|
|
91
|
+
const name = tool.name;
|
|
92
|
+
if (['read_file', 'list_directory', 'tree', 'file_search', 'code_search', 'grep'].includes(name) &&
|
|
93
|
+
['read', 'file', 'inspect', 'search', 'grep', '看', '读', '查', '搜索', '文件', '代码'].some(k => queryLc.includes(k))) {
|
|
94
|
+
score += 4;
|
|
95
|
+
}
|
|
96
|
+
if (['write_file', 'edit_file', 'move_file', 'copy_file', 'delete_file'].includes(name) &&
|
|
97
|
+
['write', 'edit', 'modify', 'fix', 'save', '生成', '写', '改', '修复', '保存', '删除'].some(k => queryLc.includes(k))) {
|
|
98
|
+
score += 4;
|
|
99
|
+
}
|
|
100
|
+
if (name.startsWith('git_') &&
|
|
101
|
+
['git', 'commit', 'diff', 'branch', '提交', '分支', '差异'].some(k => queryLc.includes(k))) {
|
|
102
|
+
score += 4;
|
|
103
|
+
}
|
|
104
|
+
if (['web_search', 'fetch_page', 'http_get'].includes(name) &&
|
|
105
|
+
['web', 'url', 'http', 'research', '搜索', '网页', '联网', '资料'].some(k => queryLc.includes(k))) {
|
|
106
|
+
score += 4;
|
|
107
|
+
}
|
|
108
|
+
if (['list_skills', 'use_skill'].includes(name) &&
|
|
109
|
+
['skill', '能力', '技能', 'ppt', 'pdf', 'excel', 'xlsx', 'docx'].some(k => queryLc.includes(k))) {
|
|
110
|
+
score += 4;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return score;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Return up to ~topK tool names ordered by relevance to the query.
|
|
118
|
+
*
|
|
119
|
+
* Always-included infrastructure tools and mustInclude (e.g. active
|
|
120
|
+
* skill required_tools) are appended regardless of score. When the candidate
|
|
121
|
+
* set is already small (<= topK + |mustInclude|), no filtering is applied.
|
|
122
|
+
*
|
|
123
|
+
* A short or empty query means we have no signal to filter — returning the
|
|
124
|
+
* full candidate set is correct in that case.
|
|
125
|
+
*/
|
|
126
|
+
export function selectRelevantTools(
|
|
127
|
+
registry: ToolRegistry,
|
|
128
|
+
candidateNames: string[],
|
|
129
|
+
query: string,
|
|
130
|
+
options?: {
|
|
131
|
+
topK?: number;
|
|
132
|
+
mustInclude?: Set<string>;
|
|
133
|
+
}
|
|
134
|
+
): string[] {
|
|
135
|
+
const topK = options?.topK ?? 12;
|
|
136
|
+
const explicitMust = new Set(options?.mustInclude ?? []);
|
|
137
|
+
|
|
138
|
+
const infraPresent = candidateNames.filter(n => INFRA_TOOLS.has(n) && !explicitMust.has(n));
|
|
139
|
+
const mustPresent = candidateNames.filter(n => explicitMust.has(n));
|
|
140
|
+
const remaining = candidateNames.filter(n => !explicitMust.has(n) && !INFRA_TOOLS.has(n));
|
|
141
|
+
|
|
142
|
+
const queryTokens = tokenize(query);
|
|
143
|
+
const queryLc = (query || '').toLowerCase();
|
|
144
|
+
const smallSurface = candidateNames.length <= topK + mustPresent.length + infraPresent.length;
|
|
145
|
+
const lowSignal = queryTokens.size < 2 && queryLc.length < 8;
|
|
146
|
+
|
|
147
|
+
// No filtering when the set is already small or the query is too short
|
|
148
|
+
if (smallSurface || lowSignal) {
|
|
149
|
+
return [...mustPresent, ...infraPresent, ...remaining];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const scored: Array<{ name: string; score: number; penalty: number }> = [];
|
|
153
|
+
const allScoredNames = [...infraPresent, ...remaining];
|
|
154
|
+
|
|
155
|
+
for (const name of allScoredNames) {
|
|
156
|
+
const tool = registry.get(name);
|
|
157
|
+
if (!tool) continue;
|
|
158
|
+
const score = scoreTool(tool, queryTokens, queryLc);
|
|
159
|
+
const penalty = MUTATING_TOOLS.has(name) && score === 0 ? 1 : 0;
|
|
160
|
+
scored.push({ name, score, penalty });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Stable sort by descending score; zero-score tools only fill spare slots
|
|
164
|
+
scored.sort((a, b) => {
|
|
165
|
+
if (a.score !== b.score) return b.score - a.score;
|
|
166
|
+
return a.penalty - b.penalty;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const budget = Math.max(0, topK - mustPresent.length);
|
|
170
|
+
const picked: string[] = [];
|
|
171
|
+
const pickedSet = new Set<string>();
|
|
172
|
+
|
|
173
|
+
for (const item of scored) {
|
|
174
|
+
if (picked.length >= budget) break;
|
|
175
|
+
if (item.score > 0) {
|
|
176
|
+
picked.push(item.name);
|
|
177
|
+
pickedSet.add(item.name);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Fill remaining slots with zero-score tools
|
|
182
|
+
if (picked.length < budget) {
|
|
183
|
+
for (const item of scored) {
|
|
184
|
+
if (picked.length >= budget) break;
|
|
185
|
+
if (!pickedSet.has(item.name)) {
|
|
186
|
+
picked.push(item.name);
|
|
187
|
+
pickedSet.add(item.name);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return [...mustPresent, ...picked];
|
|
193
|
+
}
|