Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion .blade/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,26 @@
"Bash(git status)",
"Bash(git diff *)",
"Bash(ls -l)",
"Write(**/*.md)"
"Write(**/*.md)",
"Bash(ls -la *)",
"Bash(bun install *)",
"Skill",
"Write(**/*.py)",
"Bash(python test_echovic_website.py)",
"Bash(pip install *)",
"Bash(playwright install *)",
"Bash(python test_echovic_comprehensive.py)",
"Bash(curl -s *)",
"Bash(curl -I *)",
"Edit(**/*.py)",
"Bash(python -c *)",
"Bash(python test_echovic_debug.py)",
"Bash(python test_echovic_fixed.py)",
"Bash(python test_echovic_wait.py)",
"Bash(python test_echovic_basic.py)",
"Bash(python test_echovic_individual.py)",
"Bash(python test_echovic_analysis.py)",
"Bash(python3 -c *)"
],
"ask": [],
"deny": []
Expand Down
6 changes: 5 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@
"WebFetch(domain:www.npmjs.com)",
"WebFetch(domain:deepwiki.com)",
"Bash(timeout 3 npm run start:*)",
"Bash(gtimeout 3 npm run start:*)"
"Bash(gtimeout 3 npm run start:*)",
"WebFetch(domain:leehanchung.github.io)",
"WebFetch(domain:simonwillison.net)",
"WebFetch(domain:scottspence.com)",
"WebFetch(domain:docs.anthropic.com)"
],
"deny": [],
"ask": [],
Expand Down
131 changes: 128 additions & 3 deletions src/agent/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,21 @@ import type {
LoopResult,
UserMessageContent,
} from './types.js';
import { discoverSkills, injectSkillsMetadata } from '../skills/index.js';

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

/**
* Skill 执行上下文
* 用于跟踪当前活动的 Skill 及其工具限制
*/
interface SkillExecutionContext {
skillName: string;
allowedTools?: string[];
basePath: string;
}

export class Agent {
private config: BladeConfig;
private runtimeOptions: AgentOptions;
Expand All @@ -76,6 +87,9 @@ export class Agent {
private executionEngine!: ExecutionEngine;
private attachmentCollector?: AttachmentCollector;

// Skill 执行上下文(用于 allowed-tools 限制)
private activeSkillContext?: SkillExecutionContext;

constructor(
config: BladeConfig,
runtimeOptions: AgentOptions = {},
Expand Down Expand Up @@ -171,7 +185,10 @@ export class Agent {
// 3. 加载 subagent 配置
await this.loadSubagents();

// 4. 初始化核心组件
// 4. 发现并注册 Skills
await this.discoverSkills();

// 5. 初始化核心组件
// 获取当前模型配置(从 Store)
const modelConfig = getCurrentModel();
if (!modelConfig) {
Expand Down Expand Up @@ -468,7 +485,11 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
// 根据 permissionMode 决定工具暴露策略(单一信息源:ToolRegistry.getFunctionDeclarationsByMode)
const registry = this.executionPipeline.getRegistry();
const permissionMode = context.permissionMode as PermissionMode | undefined;
const tools = registry.getFunctionDeclarationsByMode(permissionMode);
let rawTools = registry.getFunctionDeclarationsByMode(permissionMode);
// 注入 Skills 元数据到 Skill 工具的 <available_skills> 占位符
rawTools = injectSkillsMetadata(rawTools);
// 应用 Skill 的 allowed-tools 限制(如果有活动的 Skill)
const tools = this.applySkillToolRestrictions(rawTools);
const isPlanMode = permissionMode === PermissionMode.PLAN;

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

// 如果是 Skill 工具,设置执行上下文(用于 allowed-tools 限制)
if (toolCall.function.name === 'Skill' && result.success && result.metadata) {
const metadata = result.metadata as Record<string, unknown>;
if (metadata.skillName) {
this.activeSkillContext = {
skillName: metadata.skillName as string,
allowedTools: metadata.allowedTools as string[] | undefined,
basePath: (metadata.basePath as string) || '',
};
logger.debug(
`🎯 Skill "${this.activeSkillContext.skillName}" activated` +
(this.activeSkillContext.allowedTools
? ` with allowed tools: ${this.activeSkillContext.allowedTools.join(', ')}`
: '')
);
}
}

// 添加工具执行结果到消息历史
// 优先使用 llmContent(为 LLM 准备的详细内容),displayContent 仅用于终端显示
let toolResultContent = result.success
Expand Down Expand Up @@ -1673,6 +1712,92 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
}
}

/**
* 发现并注册 Skills
* Skills 是动态 Prompt 扩展机制,允许 AI 根据用户请求自动调用专业能力
*/
private async discoverSkills(): Promise<void> {
try {
const result = await discoverSkills({
cwd: process.cwd(),
});

if (result.skills.length > 0) {
logger.debug(
`✅ Discovered ${result.skills.length} skills: ${result.skills.map((s) => s.name).join(', ')}`
);
} else {
logger.debug('📦 No skills configured');
}

// 记录发现过程中的错误(不阻塞初始化)
for (const error of result.errors) {
logger.warn(`⚠️ Skill loading error at ${error.path}: ${error.error}`);
}
} catch (error) {
logger.warn('Failed to discover skills:', error);
// 不抛出错误,允许 Agent 继续初始化
}
}

/**
* 应用 Skill 的 allowed-tools 限制
* 如果有活动的 Skill 且定义了 allowed-tools,则过滤可用工具列表
*
* @param tools - 原始工具列表
* @returns 过滤后的工具列表
*/
private applySkillToolRestrictions(
tools: import('../tools/types/index.js').FunctionDeclaration[]
): import('../tools/types/index.js').FunctionDeclaration[] {
// 如果没有活动的 Skill,或者 Skill 没有定义 allowed-tools,返回原始工具列表
if (!this.activeSkillContext?.allowedTools) {
return tools;
}

const allowedTools = this.activeSkillContext.allowedTools;
logger.debug(
`🔒 Applying Skill tool restrictions: ${allowedTools.join(', ')}`
);

// 过滤工具列表,只保留 allowed-tools 中指定的工具
const filteredTools = tools.filter((tool) => {
// 检查工具名称是否在 allowed-tools 列表中
// 支持精确匹配和通配符模式(如 Bash(git:*))
return allowedTools.some((allowed) => {
// 精确匹配
if (allowed === tool.name) {
return true;
}

// 通配符匹配:Bash(git:*) 匹配 Bash
const match = allowed.match(/^(\w+)\(.*\)$/);
if (match && match[1] === tool.name) {
return true;
}

return false;
});
});

logger.debug(
`🔒 Filtered tools: ${filteredTools.map((t) => t.name).join(', ')} (${filteredTools.length}/${tools.length})`
);

return filteredTools;
}

/**
* 清除 Skill 执行上下文
* 当 Skill 执行完成或需要重置时调用
*/
public clearSkillContext(): void {
if (this.activeSkillContext) {
logger.debug(`🎯 Skill "${this.activeSkillContext.skillName}" deactivated`);
this.activeSkillContext = undefined;
}
}

/**
* 处理 @ 文件提及(支持纯文本和多模态消息)
* 从用户消息中提取 @ 提及,读取文件内容,并追加到消息
Expand Down Expand Up @@ -1781,7 +1906,7 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
}

/**
* 处理 @ 文件提及(纯文本版本)
* 处理 @ 文件提及
* 从用户消息中提取 @ 提及,读取文件内容,并追加到消息
*
* @param message - 原始用户消息
Expand Down
28 changes: 27 additions & 1 deletion src/prompts/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@
import { promises as fs } from 'fs';
import path from 'path';
import { PermissionMode } from '../config/types.js';
import { getSkillRegistry } from '../skills/index.js';
import { getEnvironmentContext } from '../utils/environment.js';
import { DEFAULT_SYSTEM_PROMPT, PLAN_MODE_SYSTEM_PROMPT } from './default.js';

/** available_skills 占位符的正则表达式 */
const AVAILABLE_SKILLS_REGEX = /<available_skills>\s*<\/available_skills>/;

/**
* 提示词构建选项
*/
Expand Down Expand Up @@ -141,11 +145,33 @@ export async function buildSystemPrompt(
}

// 组合各部分
const prompt = parts.join('\n\n---\n\n');
let prompt = parts.join('\n\n---\n\n');

// 注入 Skills 元数据到 <available_skills> 占位符
prompt = injectSkillsToPrompt(prompt);

return { prompt, sources };
}

/**
* 注入 Skills 列表到系统提示的 <available_skills> 占位符
*/
function injectSkillsToPrompt(prompt: string): string {
const registry = getSkillRegistry();
const skillsList = registry.generateAvailableSkillsList();

// 如果没有 skills,保持占位符为空(但保留标签结构)
if (!skillsList) {
return prompt;
}

// 替换占位符
return prompt.replace(
AVAILABLE_SKILLS_REGEX,
`<available_skills>\n${skillsList}\n</available_skills>`
);
}

/**
* 加载项目 BLADE.md 配置
*/
Expand Down
10 changes: 10 additions & 0 deletions src/prompts/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,16 @@ The user will primarily request you perform software engineering tasks. This inc
- 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.
- Use specialized tools instead of bash commands: Read for files, Edit for editing, Write for creating. Reserve Bash for system commands only.

## Skills
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.

How to invoke skills:
- Use the Skill tool with the skill name
- Example: \`skill: "commit-message"\` to generate commit messages

<available_skills>
</available_skills>

## Code References

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.
Expand Down
Loading
Loading