feat: Skills execution system (Phase 2B)
- SkillTool: execute skills by name via forked sub-agent - BundledSkills: 4 built-in skills (verify, debug, review, commit) - SkillLoader: bundled skills auto-loaded + budget-aware prompt listing - /skills command: detail view with /skills <name> - Budget management: 8K char budget, 250 char per-entry cap Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -31,6 +31,24 @@ public class SkillsCommand implements SlashCommand {
|
|||||||
SkillLoader loader = new SkillLoader(projectDir);
|
SkillLoader loader = new SkillLoader(projectDir);
|
||||||
List<SkillLoader.Skill> skills = loader.loadAll();
|
List<SkillLoader.Skill> skills = loader.loadAll();
|
||||||
|
|
||||||
|
// If args provided, show specific skill detail
|
||||||
|
if (args != null && !args.isBlank()) {
|
||||||
|
String query = args.strip().toLowerCase();
|
||||||
|
var match = skills.stream()
|
||||||
|
.filter(s -> s.name().equalsIgnoreCase(query))
|
||||||
|
.findFirst();
|
||||||
|
if (match.isEmpty()) {
|
||||||
|
match = skills.stream()
|
||||||
|
.filter(s -> s.name().toLowerCase().contains(query))
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
if (match.isPresent()) {
|
||||||
|
return formatSkillDetail(match.get());
|
||||||
|
}
|
||||||
|
return AnsiStyle.red(" Skill not found: " + args.strip()) + "\n"
|
||||||
|
+ AnsiStyle.dim(" Use /skills to list all available skills.");
|
||||||
|
}
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("\n");
|
sb.append("\n");
|
||||||
sb.append(AnsiStyle.bold(" 🎯 Available Skills\n"));
|
sb.append(AnsiStyle.bold(" 🎯 Available Skills\n"));
|
||||||
@@ -61,12 +79,43 @@ public class SkillsCommand implements SlashCommand {
|
|||||||
if (!skill.whenToUse().isEmpty()) {
|
if (!skill.whenToUse().isEmpty()) {
|
||||||
sb.append(" ").append(AnsiStyle.dim("When: " + skill.whenToUse())).append("\n");
|
sb.append(" ").append(AnsiStyle.dim("When: " + skill.whenToUse())).append("\n");
|
||||||
}
|
}
|
||||||
sb.append(" ").append(AnsiStyle.dim("File: " + skill.filePath())).append("\n");
|
|
||||||
sb.append("\n");
|
sb.append("\n");
|
||||||
}
|
}
|
||||||
sb.append(AnsiStyle.dim(" Total " + skills.size() + " skills\n"));
|
sb.append(AnsiStyle.dim(" Total " + skills.size() + " skills"));
|
||||||
|
sb.append(AnsiStyle.dim(" • Use /skills <name> for details\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String formatSkillDetail(SkillLoader.Skill skill) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("\n");
|
||||||
|
sb.append(AnsiStyle.bold(" 🎯 Skill: " + skill.name())).append("\n");
|
||||||
|
sb.append(" ").append("─".repeat(50)).append("\n\n");
|
||||||
|
|
||||||
|
sb.append(" ").append(AnsiStyle.bold("Source: ")).append(skill.source()).append("\n");
|
||||||
|
if (!skill.description().isEmpty()) {
|
||||||
|
sb.append(" ").append(AnsiStyle.bold("Description: ")).append(skill.description()).append("\n");
|
||||||
|
}
|
||||||
|
if (!skill.whenToUse().isEmpty()) {
|
||||||
|
sb.append(" ").append(AnsiStyle.bold("When to use: ")).append(skill.whenToUse()).append("\n");
|
||||||
|
}
|
||||||
|
sb.append(" ").append(AnsiStyle.bold("File: ")).append(skill.filePath()).append("\n");
|
||||||
|
sb.append("\n");
|
||||||
|
|
||||||
|
// Show content preview
|
||||||
|
String content = skill.content();
|
||||||
|
if (content.length() > 500) {
|
||||||
|
content = content.substring(0, 497) + "...";
|
||||||
|
}
|
||||||
|
sb.append(AnsiStyle.dim(" Content:\n"));
|
||||||
|
for (String line : content.lines().toList()) {
|
||||||
|
sb.append(AnsiStyle.dim(" │ " + line)).append("\n");
|
||||||
|
}
|
||||||
|
sb.append("\n");
|
||||||
|
sb.append(AnsiStyle.dim(" Tip: Ask AI to execute this skill or type: /verify, /debug, etc.\n"));
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,8 @@ public class AppConfig {
|
|||||||
new SleepTool(),
|
new SleepTool(),
|
||||||
new ToolSearchTool(),
|
new ToolSearchTool(),
|
||||||
new EnterPlanModeTool(),
|
new EnterPlanModeTool(),
|
||||||
new ExitPlanModeTool()
|
new ExitPlanModeTool(),
|
||||||
|
new SkillTool()
|
||||||
);
|
);
|
||||||
|
|
||||||
// P2: 注册 MCP 工具桥接(将远程 MCP 工具映射为本地工具)
|
// P2: 注册 MCP 工具桥接(将远程 MCP 工具映射为本地工具)
|
||||||
@@ -238,7 +239,7 @@ public class AppConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public String systemPrompt() {
|
public String systemPrompt(ToolContext toolContext) {
|
||||||
Path projectDir = Path.of(System.getProperty("user.dir"));
|
Path projectDir = Path.of(System.getProperty("user.dir"));
|
||||||
|
|
||||||
ClaudeMdLoader claudeLoader = new ClaudeMdLoader(projectDir);
|
ClaudeMdLoader claudeLoader = new ClaudeMdLoader(projectDir);
|
||||||
@@ -248,6 +249,9 @@ public class AppConfig {
|
|||||||
skillLoader.loadAll();
|
skillLoader.loadAll();
|
||||||
String skillsSummary = skillLoader.buildSkillsSummary();
|
String skillsSummary = skillLoader.buildSkillsSummary();
|
||||||
|
|
||||||
|
// Inject SkillLoader into ToolContext for SkillTool
|
||||||
|
toolContext.set(SkillTool.SKILL_LOADER_KEY, skillLoader);
|
||||||
|
|
||||||
GitContext gitContext = new GitContext(projectDir).collect();
|
GitContext gitContext = new GitContext(projectDir).collect();
|
||||||
String gitSummary = gitContext.buildSummary();
|
String gitSummary = gitContext.buildSummary();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
package com.claudecode.context;
|
||||||
|
|
||||||
|
import com.claudecode.context.SkillLoader.Skill;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内置 Skills 注册 —— 对应 claude-code/src/skills/bundled/ 目录。
|
||||||
|
* <p>
|
||||||
|
* 提供默认可用的 Skills,无需用户手动创建 .md 文件。
|
||||||
|
*/
|
||||||
|
public final class BundledSkills {
|
||||||
|
|
||||||
|
private BundledSkills() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有内置 Skills。
|
||||||
|
*/
|
||||||
|
public static List<Skill> getAll() {
|
||||||
|
return List.of(
|
||||||
|
verifySkill(),
|
||||||
|
debugSkill(),
|
||||||
|
reviewSkill(),
|
||||||
|
commitSkill()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* verify — 代码验证 Skill。
|
||||||
|
* 运行项目测试、lint、类型检查,确保代码变更正确。
|
||||||
|
*/
|
||||||
|
static Skill verifySkill() {
|
||||||
|
return new Skill(
|
||||||
|
"verify",
|
||||||
|
"Run tests and checks to verify code changes are correct",
|
||||||
|
"After making code changes that need verification",
|
||||||
|
"""
|
||||||
|
# Verify Changes
|
||||||
|
|
||||||
|
You are verifying that recent code changes are correct and don't break anything.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. **Identify the project type** by reading build/config files:
|
||||||
|
- Look for: package.json, pom.xml, build.gradle, Cargo.toml, go.mod, etc.
|
||||||
|
|
||||||
|
2. **Run the appropriate test command**:
|
||||||
|
- Node.js: `npm test` or `npx jest` or `npx vitest`
|
||||||
|
- Java/Maven: `mvn test -q`
|
||||||
|
- Java/Gradle: `./gradlew test`
|
||||||
|
- Python: `pytest` or `python -m pytest`
|
||||||
|
- Rust: `cargo test`
|
||||||
|
- Go: `go test ./...`
|
||||||
|
|
||||||
|
3. **Run linting if available**:
|
||||||
|
- Node.js: `npm run lint` or `npx eslint .`
|
||||||
|
- Python: `ruff check .` or `flake8`
|
||||||
|
- Java: `mvn checkstyle:check -q`
|
||||||
|
|
||||||
|
4. **Run type checking if available**:
|
||||||
|
- TypeScript: `npx tsc --noEmit`
|
||||||
|
- Python: `mypy .`
|
||||||
|
|
||||||
|
5. **Report results**:
|
||||||
|
- ✅ All checks passed
|
||||||
|
- ❌ Failures found (include specific errors)
|
||||||
|
- ⚠️ Warnings (include details)
|
||||||
|
|
||||||
|
If a check fails, analyze the error and suggest a fix.
|
||||||
|
""",
|
||||||
|
"bundled",
|
||||||
|
Path.of("bundled://verify")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* debug — 调试辅助 Skill。
|
||||||
|
* 分析错误信息,定位问题根因。
|
||||||
|
*/
|
||||||
|
static Skill debugSkill() {
|
||||||
|
return new Skill(
|
||||||
|
"debug",
|
||||||
|
"Analyze errors and help debug issues",
|
||||||
|
"When encountering an error or unexpected behavior",
|
||||||
|
"""
|
||||||
|
# Debug Issue
|
||||||
|
|
||||||
|
You are helping debug an issue. Follow this systematic approach:
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. **Understand the error**:
|
||||||
|
- Read the error message carefully
|
||||||
|
- Identify the error type (compile, runtime, logic, etc.)
|
||||||
|
- Note the file and line number if available
|
||||||
|
|
||||||
|
2. **Reproduce the issue**:
|
||||||
|
- Try to reproduce the error with the minimal command
|
||||||
|
- Check if the error is consistent
|
||||||
|
|
||||||
|
3. **Trace the root cause**:
|
||||||
|
- Read the file(s) mentioned in the error
|
||||||
|
- Check imports, dependencies, and configuration
|
||||||
|
- Look for recent changes that might have caused it
|
||||||
|
- Use Grep to find related code patterns
|
||||||
|
|
||||||
|
4. **Analyze and diagnose**:
|
||||||
|
- Identify the root cause (not just symptoms)
|
||||||
|
- Consider edge cases and dependencies
|
||||||
|
- Check if similar patterns exist elsewhere
|
||||||
|
|
||||||
|
5. **Suggest fix**:
|
||||||
|
- Propose a specific, minimal fix
|
||||||
|
- Explain why the fix works
|
||||||
|
- Note any side effects or risks
|
||||||
|
|
||||||
|
If the user provides an error message, start from Step 1.
|
||||||
|
If the user describes unexpected behavior, start from Step 2.
|
||||||
|
""",
|
||||||
|
"bundled",
|
||||||
|
Path.of("bundled://debug")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* review — 代码审查 Skill。
|
||||||
|
*/
|
||||||
|
static Skill reviewSkill() {
|
||||||
|
return new Skill(
|
||||||
|
"review",
|
||||||
|
"Review code changes for quality, bugs, and best practices",
|
||||||
|
"When the user wants feedback on code changes",
|
||||||
|
"""
|
||||||
|
# Code Review
|
||||||
|
|
||||||
|
You are reviewing code changes. Focus on:
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
1. **Correctness**: Does the code do what it's supposed to?
|
||||||
|
2. **Edge cases**: Are boundary conditions handled?
|
||||||
|
3. **Error handling**: Are errors caught and handled gracefully?
|
||||||
|
4. **Security**: Any injection, auth, or data exposure risks?
|
||||||
|
5. **Performance**: Any obvious performance issues?
|
||||||
|
6. **Readability**: Is the code clear and well-structured?
|
||||||
|
7. **Tests**: Are there tests? Do they cover key cases?
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
1. Run `git diff` to see what changed
|
||||||
|
2. Read each changed file
|
||||||
|
3. For each issue found, note:
|
||||||
|
- Severity: 🔴 Critical / 🟡 Warning / 🔵 Suggestion
|
||||||
|
- File and line number
|
||||||
|
- What's wrong and how to fix it
|
||||||
|
4. Summarize findings
|
||||||
|
""",
|
||||||
|
"bundled",
|
||||||
|
Path.of("bundled://review")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* commit — 提交 Skill。
|
||||||
|
*/
|
||||||
|
static Skill commitSkill() {
|
||||||
|
return new Skill(
|
||||||
|
"commit",
|
||||||
|
"Create a well-structured git commit with conventional commit message",
|
||||||
|
"When the user wants to commit changes",
|
||||||
|
"""
|
||||||
|
# Git Commit
|
||||||
|
|
||||||
|
Create a well-structured git commit.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. Run `git status` and `git diff --stat` to see changes
|
||||||
|
2. Analyze the changes to determine:
|
||||||
|
- Type: feat, fix, refactor, docs, test, chore, etc.
|
||||||
|
- Scope: which module/area is affected
|
||||||
|
- Breaking changes
|
||||||
|
3. Generate commit message in Conventional Commits format:
|
||||||
|
```
|
||||||
|
type(scope): brief description
|
||||||
|
|
||||||
|
Detailed body explaining what and why (not how).
|
||||||
|
|
||||||
|
Breaking changes (if any).
|
||||||
|
```
|
||||||
|
4. Stage relevant files with `git add`
|
||||||
|
5. Create the commit with `git commit -m "..."`
|
||||||
|
6. Report the commit hash
|
||||||
|
""",
|
||||||
|
"bundled",
|
||||||
|
Path.of("bundled://commit")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,10 @@ public class SkillLoader {
|
|||||||
public List<Skill> loadAll() {
|
public List<Skill> loadAll() {
|
||||||
skills.clear();
|
skills.clear();
|
||||||
|
|
||||||
|
// 0. 内置技能
|
||||||
|
skills.addAll(BundledSkills.getAll());
|
||||||
|
log.debug("Loaded {} bundled skills", BundledSkills.getAll().size());
|
||||||
|
|
||||||
// 1. 用户级技能
|
// 1. 用户级技能
|
||||||
Path userSkillsDir = Path.of(System.getProperty("user.home"), ".claude", "skills");
|
Path userSkillsDir = Path.of(System.getProperty("user.home"), ".claude", "skills");
|
||||||
loadFromDirectory(userSkillsDir, "user");
|
loadFromDirectory(userSkillsDir, "user");
|
||||||
@@ -147,25 +151,58 @@ public class SkillLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建技能上下文摘要(注入系统提示词)
|
* 构建技能上下文摘要(注入系统提示词)。
|
||||||
|
* 支持预算控制,确保不超过上下文窗口的 1%。
|
||||||
*/
|
*/
|
||||||
public String buildSkillsSummary() {
|
public String buildSkillsSummary() {
|
||||||
|
return buildSkillsSummary(8000); // Default 8K chars budget
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建技能上下文摘要(带预算控制)。
|
||||||
|
* 对应 TS formatCommandsWithinBudget()。
|
||||||
|
*
|
||||||
|
* @param charBudget 最大字符预算
|
||||||
|
*/
|
||||||
|
public String buildSkillsSummary(int charBudget) {
|
||||||
if (skills.isEmpty()) {
|
if (skills.isEmpty()) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("# Available Skills\n\n");
|
sb.append("# Available Skills\n\n");
|
||||||
|
sb.append("Skills can be invoked by name using the Skill tool or by typing /<name>.\n\n");
|
||||||
|
|
||||||
|
int budgetUsed = sb.length();
|
||||||
|
int perEntryMax = 250; // Per-entry cap for cache efficiency
|
||||||
|
|
||||||
for (Skill skill : skills) {
|
for (Skill skill : skills) {
|
||||||
sb.append("- **").append(skill.name()).append("**");
|
StringBuilder entry = new StringBuilder();
|
||||||
|
entry.append("- **").append(skill.name()).append("**");
|
||||||
if (!skill.description().isEmpty()) {
|
if (!skill.description().isEmpty()) {
|
||||||
sb.append(": ").append(skill.description());
|
String desc = skill.description();
|
||||||
|
if (desc.length() > perEntryMax - skill.name().length() - 10) {
|
||||||
|
desc = desc.substring(0, perEntryMax - skill.name().length() - 13) + "...";
|
||||||
|
}
|
||||||
|
entry.append(": ").append(desc);
|
||||||
}
|
}
|
||||||
if (!skill.whenToUse().isEmpty()) {
|
if (!skill.whenToUse().isEmpty()) {
|
||||||
sb.append(" (use when: ").append(skill.whenToUse()).append(")");
|
entry.append(" (use when: ").append(skill.whenToUse()).append(")");
|
||||||
}
|
}
|
||||||
sb.append("\n");
|
entry.append("\n");
|
||||||
|
|
||||||
|
// Check budget
|
||||||
|
if (budgetUsed + entry.length() > charBudget) {
|
||||||
|
// Add truncation notice
|
||||||
|
sb.append("- ... and ").append(skills.size() - skills.indexOf(skill))
|
||||||
|
.append(" more skills (use /skills to see all)\n");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sb.append(entry);
|
||||||
|
budgetUsed += entry.length();
|
||||||
|
}
|
||||||
|
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package com.claudecode.tool.impl;
|
||||||
|
|
||||||
|
import com.claudecode.context.SkillLoader;
|
||||||
|
import com.claudecode.context.SkillLoader.Skill;
|
||||||
|
import com.claudecode.tool.Tool;
|
||||||
|
import com.claudecode.tool.ToolContext;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skill 执行工具 —— 对应 claude-code/src/tools/SkillTool/SkillTool.ts。
|
||||||
|
* <p>
|
||||||
|
* 通过名称调用已加载的 Skill。Skill 会作为 forked sub-agent 执行,
|
||||||
|
* 注入 Skill 的 markdown 内容作为上下文指导。
|
||||||
|
* <p>
|
||||||
|
* Skills 来源:
|
||||||
|
* <ul>
|
||||||
|
* <li>用户级: ~/.claude/skills/</li>
|
||||||
|
* <li>项目级: ./.claude/skills/</li>
|
||||||
|
* <li>命令目录: ./.claude/commands/</li>
|
||||||
|
* <li>内置 Skills: verify, debug 等</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class SkillTool implements Tool {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(SkillTool.class);
|
||||||
|
|
||||||
|
/** ToolContext key for SkillLoader */
|
||||||
|
public static final String SKILL_LOADER_KEY = "SKILL_LOADER";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return "Skill";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String description() {
|
||||||
|
return """
|
||||||
|
Execute a registered skill by name. Skills are reusable, structured workflows \
|
||||||
|
defined in markdown files that guide a specialized sub-agent.
|
||||||
|
|
||||||
|
When to use:
|
||||||
|
- User invokes a skill by name (e.g., /verify, /debug)
|
||||||
|
- You identify a task that matches a registered skill's "whenToUse" criteria
|
||||||
|
- Complex workflows that benefit from structured guidance
|
||||||
|
|
||||||
|
Available skills are listed in the system prompt under "Available Skills".
|
||||||
|
|
||||||
|
The skill runs as an isolated sub-agent with its own context. Provide any relevant \
|
||||||
|
arguments or context from the current conversation.
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String inputSchema() {
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"skill_name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Name of the skill to execute (case-insensitive)"
|
||||||
|
},
|
||||||
|
"arguments": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Arguments or context to pass to the skill"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["skill_name"]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String execute(Map<String, Object> input, ToolContext context) {
|
||||||
|
String skillName = (String) input.get("skill_name");
|
||||||
|
String arguments = (String) input.getOrDefault("arguments", "");
|
||||||
|
|
||||||
|
if (skillName == null || skillName.isBlank()) {
|
||||||
|
return "Error: 'skill_name' is required";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get SkillLoader from context
|
||||||
|
SkillLoader skillLoader = context.get(SKILL_LOADER_KEY);
|
||||||
|
if (skillLoader == null) {
|
||||||
|
return "Error: SkillLoader not configured. No skills available.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find skill by name
|
||||||
|
Optional<Skill> skillOpt = skillLoader.findByName(skillName);
|
||||||
|
if (skillOpt.isEmpty()) {
|
||||||
|
// Try partial match
|
||||||
|
skillOpt = findByPartialName(skillLoader.getSkills(), skillName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skillOpt.isEmpty()) {
|
||||||
|
StringBuilder msg = new StringBuilder();
|
||||||
|
msg.append("Skill '").append(skillName).append("' not found.\n\n");
|
||||||
|
msg.append("Available skills:\n");
|
||||||
|
for (Skill s : skillLoader.getSkills()) {
|
||||||
|
msg.append(" - ").append(s.name());
|
||||||
|
if (!s.description().isEmpty()) {
|
||||||
|
msg.append(": ").append(s.description());
|
||||||
|
}
|
||||||
|
msg.append("\n");
|
||||||
|
}
|
||||||
|
return msg.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Skill skill = skillOpt.get();
|
||||||
|
log.info("Executing skill: {} [{}]", skill.name(), skill.source());
|
||||||
|
|
||||||
|
// Build skill execution prompt
|
||||||
|
String skillPrompt = buildSkillPrompt(skill, arguments);
|
||||||
|
|
||||||
|
// Execute via agent factory (same as AgentTool)
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
java.util.function.Function<String, String> agentFactory =
|
||||||
|
context.getOrDefault(AgentTool.AGENT_FACTORY_KEY, null);
|
||||||
|
|
||||||
|
if (agentFactory == null) {
|
||||||
|
// Fallback: return skill content for manual execution guidance
|
||||||
|
return "⚠️ Sub-agent not available. Skill content for manual execution:\n\n"
|
||||||
|
+ "# Skill: " + skill.name() + "\n"
|
||||||
|
+ skill.content();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String result = agentFactory.apply(skillPrompt);
|
||||||
|
log.info("Skill '{}' completed, result: {} chars", skill.name(), result.length());
|
||||||
|
return result;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Skill execution failed", e);
|
||||||
|
return "Error executing skill '" + skill.name() + "': " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReadOnly() {
|
||||||
|
return false; // Skills may modify files
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String activityDescription(Map<String, Object> input) {
|
||||||
|
String name = input != null ? (String) input.get("skill_name") : null;
|
||||||
|
return name != null ? "Running skill: " + name + "..." : "Running skill...";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the full prompt for skill execution.
|
||||||
|
*/
|
||||||
|
private String buildSkillPrompt(Skill skill, String arguments) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("You are executing a skill: ").append(skill.name()).append("\n\n");
|
||||||
|
|
||||||
|
if (!skill.description().isEmpty()) {
|
||||||
|
sb.append("Description: ").append(skill.description()).append("\n");
|
||||||
|
}
|
||||||
|
if (!skill.whenToUse().isEmpty()) {
|
||||||
|
sb.append("When to use: ").append(skill.whenToUse()).append("\n");
|
||||||
|
}
|
||||||
|
sb.append("\n");
|
||||||
|
|
||||||
|
// Inject skill content as instructions
|
||||||
|
sb.append("## Skill Instructions\n\n");
|
||||||
|
sb.append(skill.content()).append("\n\n");
|
||||||
|
|
||||||
|
// Inject arguments
|
||||||
|
if (arguments != null && !arguments.isBlank()) {
|
||||||
|
sb.append("## User Arguments\n\n");
|
||||||
|
sb.append(arguments).append("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append("""
|
||||||
|
## Execution Guidelines
|
||||||
|
- Follow the skill instructions above carefully
|
||||||
|
- Use the available tools to complete the task
|
||||||
|
- Report results concisely when done
|
||||||
|
- If the skill requires user input, use AskUserQuestion
|
||||||
|
""");
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Partial name match for skills.
|
||||||
|
*/
|
||||||
|
private Optional<Skill> findByPartialName(List<Skill> skills, String name) {
|
||||||
|
String lower = name.toLowerCase();
|
||||||
|
return skills.stream()
|
||||||
|
.filter(s -> s.name().toLowerCase().contains(lower))
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user