Skip to content

Commit 1c0ad80

Browse files
authored
Merge pull request #6 from echoVic/feature/skills-enhancement
Feature/skills enhancement
2 parents f4f0457 + af6f40f commit 1c0ad80

23 files changed

Lines changed: 1802 additions & 80 deletions

.blade/settings.local.json

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,26 @@
55
"Bash(git status)",
66
"Bash(git diff *)",
77
"Bash(ls -l)",
8-
"Write(**/*.md)"
8+
"Write(**/*.md)",
9+
"Bash(ls -la *)",
10+
"Bash(bun install *)",
11+
"Skill",
12+
"Write(**/*.py)",
13+
"Bash(python test_echovic_website.py)",
14+
"Bash(pip install *)",
15+
"Bash(playwright install *)",
16+
"Bash(python test_echovic_comprehensive.py)",
17+
"Bash(curl -s *)",
18+
"Bash(curl -I *)",
19+
"Edit(**/*.py)",
20+
"Bash(python -c *)",
21+
"Bash(python test_echovic_debug.py)",
22+
"Bash(python test_echovic_fixed.py)",
23+
"Bash(python test_echovic_wait.py)",
24+
"Bash(python test_echovic_basic.py)",
25+
"Bash(python test_echovic_individual.py)",
26+
"Bash(python test_echovic_analysis.py)",
27+
"Bash(python3 -c *)"
928
],
1029
"ask": [],
1130
"deny": []

.claude/settings.local.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@
7171
"WebFetch(domain:www.npmjs.com)",
7272
"WebFetch(domain:deepwiki.com)",
7373
"Bash(timeout 3 npm run start:*)",
74-
"Bash(gtimeout 3 npm run start:*)"
74+
"Bash(gtimeout 3 npm run start:*)",
75+
"WebFetch(domain:leehanchung.github.io)",
76+
"WebFetch(domain:simonwillison.net)",
77+
"WebFetch(domain:scottspence.com)",
78+
"WebFetch(domain:docs.anthropic.com)"
7579
],
7680
"deny": [],
7781
"ask": [],

src/agent/Agent.ts

Lines changed: 128 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,21 @@ import type {
5858
LoopResult,
5959
UserMessageContent,
6060
} from './types.js';
61+
import { discoverSkills, injectSkillsMetadata } from '../skills/index.js';
6162

6263
// 创建 Agent 专用 Logger
6364
const logger = createLogger(LogCategory.AGENT);
6465

66+
/**
67+
* Skill 执行上下文
68+
* 用于跟踪当前活动的 Skill 及其工具限制
69+
*/
70+
interface SkillExecutionContext {
71+
skillName: string;
72+
allowedTools?: string[];
73+
basePath: string;
74+
}
75+
6576
export class Agent {
6677
private config: BladeConfig;
6778
private runtimeOptions: AgentOptions;
@@ -76,6 +87,9 @@ export class Agent {
7687
private executionEngine!: ExecutionEngine;
7788
private attachmentCollector?: AttachmentCollector;
7889

90+
// Skill 执行上下文(用于 allowed-tools 限制)
91+
private activeSkillContext?: SkillExecutionContext;
92+
7993
constructor(
8094
config: BladeConfig,
8195
runtimeOptions: AgentOptions = {},
@@ -171,7 +185,10 @@ export class Agent {
171185
// 3. 加载 subagent 配置
172186
await this.loadSubagents();
173187

174-
// 4. 初始化核心组件
188+
// 4. 发现并注册 Skills
189+
await this.discoverSkills();
190+
191+
// 5. 初始化核心组件
175192
// 获取当前模型配置(从 Store)
176193
const modelConfig = getCurrentModel();
177194
if (!modelConfig) {
@@ -468,7 +485,11 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
468485
// 根据 permissionMode 决定工具暴露策略(单一信息源:ToolRegistry.getFunctionDeclarationsByMode)
469486
const registry = this.executionPipeline.getRegistry();
470487
const permissionMode = context.permissionMode as PermissionMode | undefined;
471-
const tools = registry.getFunctionDeclarationsByMode(permissionMode);
488+
let rawTools = registry.getFunctionDeclarationsByMode(permissionMode);
489+
// 注入 Skills 元数据到 Skill 工具的 <available_skills> 占位符
490+
rawTools = injectSkillsMetadata(rawTools);
491+
// 应用 Skill 的 allowed-tools 限制(如果有活动的 Skill)
492+
const tools = this.applySkillToolRestrictions(rawTools);
472493
const isPlanMode = permissionMode === PermissionMode.PLAN;
473494

474495
if (isPlanMode) {
@@ -1028,6 +1049,24 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
10281049
);
10291050
}
10301051

1052+
// 如果是 Skill 工具,设置执行上下文(用于 allowed-tools 限制)
1053+
if (toolCall.function.name === 'Skill' && result.success && result.metadata) {
1054+
const metadata = result.metadata as Record<string, unknown>;
1055+
if (metadata.skillName) {
1056+
this.activeSkillContext = {
1057+
skillName: metadata.skillName as string,
1058+
allowedTools: metadata.allowedTools as string[] | undefined,
1059+
basePath: (metadata.basePath as string) || '',
1060+
};
1061+
logger.debug(
1062+
`🎯 Skill "${this.activeSkillContext.skillName}" activated` +
1063+
(this.activeSkillContext.allowedTools
1064+
? ` with allowed tools: ${this.activeSkillContext.allowedTools.join(', ')}`
1065+
: '')
1066+
);
1067+
}
1068+
}
1069+
10311070
// 添加工具执行结果到消息历史
10321071
// 优先使用 llmContent(为 LLM 准备的详细内容),displayContent 仅用于终端显示
10331072
let toolResultContent = result.success
@@ -1673,6 +1712,92 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
16731712
}
16741713
}
16751714

1715+
/**
1716+
* 发现并注册 Skills
1717+
* Skills 是动态 Prompt 扩展机制,允许 AI 根据用户请求自动调用专业能力
1718+
*/
1719+
private async discoverSkills(): Promise<void> {
1720+
try {
1721+
const result = await discoverSkills({
1722+
cwd: process.cwd(),
1723+
});
1724+
1725+
if (result.skills.length > 0) {
1726+
logger.debug(
1727+
`✅ Discovered ${result.skills.length} skills: ${result.skills.map((s) => s.name).join(', ')}`
1728+
);
1729+
} else {
1730+
logger.debug('📦 No skills configured');
1731+
}
1732+
1733+
// 记录发现过程中的错误(不阻塞初始化)
1734+
for (const error of result.errors) {
1735+
logger.warn(`⚠️ Skill loading error at ${error.path}: ${error.error}`);
1736+
}
1737+
} catch (error) {
1738+
logger.warn('Failed to discover skills:', error);
1739+
// 不抛出错误,允许 Agent 继续初始化
1740+
}
1741+
}
1742+
1743+
/**
1744+
* 应用 Skill 的 allowed-tools 限制
1745+
* 如果有活动的 Skill 且定义了 allowed-tools,则过滤可用工具列表
1746+
*
1747+
* @param tools - 原始工具列表
1748+
* @returns 过滤后的工具列表
1749+
*/
1750+
private applySkillToolRestrictions(
1751+
tools: import('../tools/types/index.js').FunctionDeclaration[]
1752+
): import('../tools/types/index.js').FunctionDeclaration[] {
1753+
// 如果没有活动的 Skill,或者 Skill 没有定义 allowed-tools,返回原始工具列表
1754+
if (!this.activeSkillContext?.allowedTools) {
1755+
return tools;
1756+
}
1757+
1758+
const allowedTools = this.activeSkillContext.allowedTools;
1759+
logger.debug(
1760+
`🔒 Applying Skill tool restrictions: ${allowedTools.join(', ')}`
1761+
);
1762+
1763+
// 过滤工具列表,只保留 allowed-tools 中指定的工具
1764+
const filteredTools = tools.filter((tool) => {
1765+
// 检查工具名称是否在 allowed-tools 列表中
1766+
// 支持精确匹配和通配符模式(如 Bash(git:*))
1767+
return allowedTools.some((allowed) => {
1768+
// 精确匹配
1769+
if (allowed === tool.name) {
1770+
return true;
1771+
}
1772+
1773+
// 通配符匹配:Bash(git:*) 匹配 Bash
1774+
const match = allowed.match(/^(\w+)\(.*\)$/);
1775+
if (match && match[1] === tool.name) {
1776+
return true;
1777+
}
1778+
1779+
return false;
1780+
});
1781+
});
1782+
1783+
logger.debug(
1784+
`🔒 Filtered tools: ${filteredTools.map((t) => t.name).join(', ')} (${filteredTools.length}/${tools.length})`
1785+
);
1786+
1787+
return filteredTools;
1788+
}
1789+
1790+
/**
1791+
* 清除 Skill 执行上下文
1792+
* 当 Skill 执行完成或需要重置时调用
1793+
*/
1794+
public clearSkillContext(): void {
1795+
if (this.activeSkillContext) {
1796+
logger.debug(`🎯 Skill "${this.activeSkillContext.skillName}" deactivated`);
1797+
this.activeSkillContext = undefined;
1798+
}
1799+
}
1800+
16761801
/**
16771802
* 处理 @ 文件提及(支持纯文本和多模态消息)
16781803
* 从用户消息中提取 @ 提及,读取文件内容,并追加到消息
@@ -1781,7 +1906,7 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
17811906
}
17821907

17831908
/**
1784-
* 处理 @ 文件提及(纯文本版本)
1909+
* 处理 @ 文件提及
17851910
* 从用户消息中提取 @ 提及,读取文件内容,并追加到消息
17861911
*
17871912
* @param message - 原始用户消息

src/prompts/builder.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616
import { promises as fs } from 'fs';
1717
import path from 'path';
1818
import { PermissionMode } from '../config/types.js';
19+
import { getSkillRegistry } from '../skills/index.js';
1920
import { getEnvironmentContext } from '../utils/environment.js';
2021
import { DEFAULT_SYSTEM_PROMPT, PLAN_MODE_SYSTEM_PROMPT } from './default.js';
2122

23+
/** available_skills 占位符的正则表达式 */
24+
const AVAILABLE_SKILLS_REGEX = /<available_skills>\s*<\/available_skills>/;
25+
2226
/**
2327
* 提示词构建选项
2428
*/
@@ -141,11 +145,33 @@ export async function buildSystemPrompt(
141145
}
142146

143147
// 组合各部分
144-
const prompt = parts.join('\n\n---\n\n');
148+
let prompt = parts.join('\n\n---\n\n');
149+
150+
// 注入 Skills 元数据到 <available_skills> 占位符
151+
prompt = injectSkillsToPrompt(prompt);
145152

146153
return { prompt, sources };
147154
}
148155

156+
/**
157+
* 注入 Skills 列表到系统提示的 <available_skills> 占位符
158+
*/
159+
function injectSkillsToPrompt(prompt: string): string {
160+
const registry = getSkillRegistry();
161+
const skillsList = registry.generateAvailableSkillsList();
162+
163+
// 如果没有 skills,保持占位符为空(但保留标签结构)
164+
if (!skillsList) {
165+
return prompt;
166+
}
167+
168+
// 替换占位符
169+
return prompt.replace(
170+
AVAILABLE_SKILLS_REGEX,
171+
`<available_skills>\n${skillsList}\n</available_skills>`
172+
);
173+
}
174+
149175
/**
150176
* 加载项目 BLADE.md 配置
151177
*/

src/prompts/default.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,16 @@ The user will primarily request you perform software engineering tasks. This inc
140140
- You can call multiple tools in a single response. Make independent tool calls in parallel. If calls depend on previous results, run them sequentially. Never use placeholders or guess missing parameters.
141141
- Use specialized tools instead of bash commands: Read for files, Edit for editing, Write for creating. Reserve Bash for system commands only.
142142
143+
## Skills
144+
When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.
145+
146+
How to invoke skills:
147+
- Use the Skill tool with the skill name
148+
- Example: \`skill: "commit-message"\` to generate commit messages
149+
150+
<available_skills>
151+
</available_skills>
152+
143153
## Code References
144154
145155
When referencing specific functions or pieces of code include the pattern \`file_path:line_number\` to allow the user to easily navigate to the source code location.

0 commit comments

Comments
 (0)