package builtin // skill.go 实现 Skill 工具--允许 LLM 调用预定义的工作流 Skill. // // Skill 是可复用的提示词工作流,定义在 .flyto/skills/name/SKILL.md 文件中. // 调用方式:Skill{skill: "commit", args: "feat: add login"} // // 执行模式: // // inline(默认): Skill 提示词展开后作为工具结果返回,LLM 在当前上下文中处理. // fork: 在独立子 Agent 中运行,完成后返回结果文本. // // 安全设计: // - Skill 文件来自用户目录,不执行任何 Shell 命令(shell block 不支持) // - 路径遍历防护由 skill_loader.go 中的 ScanSkillsDir 负责 // - allowedTools 字段为信息性(当前版本不强制执行) // // 升华改进(ELEVATED): 早期实现 SkillTool 直接耦合 Command 类型和 React JSX. // 我们通过 SkillExecutor 接口完全解耦-- // // SkillTool 只知道接口,不知道 SkillRegistry 或 Engine 的存在, // 测试时可以注入 mock executor,无需真实 Engine. // // 替代方案:直接持有 *engine.SkillRegistry(循环依赖,无法测试). import ( "context" "encoding/json" "fmt" "strings" "git.flytoex.net/yuanwei/flyto-agent/pkg/permission" "git.flytoex.net/yuanwei/flyto-agent/pkg/tools" ) // skillInput 是 Skill 工具的输入结构体. type skillInput struct { // Skill Skill 名称(对应 skills/ 目录下的子目录名或文件名) Skill string `json:"skill"` // Args 传递给 Skill 的参数(替换 Skill 模板中的 $ARGUMENTS) Args string `json:"args,omitempty"` } // SkillExecutor 是 SkillTool 依赖的执行器接口. // // 精妙之处(CLEVER): 接口定义在 builtin 包(消费方),实现在 engine 包-- // 这与 AgentExecutor 完全相同的模式(依赖倒置原则): // builtin 定义接口和返回类型,engine 导入 builtin 来实现接口, // builtin 不需要导入 engine(避免循环依赖). // 替代方案:把接口放在 engine 包,让 builtin 导入 engine(循环依赖,不可行). type SkillExecutor interface { // InvokeSkill 执行指定 Skill,返回结果. // 参数 name/args 对应 skillInput 字段. InvokeSkill(ctx context.Context, name, args string) (*SkillResult, error) // ListSkillEntries 返回所有可用 Skill 的精简列表(用于工具描述生成). ListSkillEntries() []*SkillEntryDesc } // SkillResult 是 Skill 执行结果(对应 engine.SkillInvokeResult 的镜像). // 定义在 builtin 包避免循环依赖. type SkillResult struct { // Mode 执行模式:"inline" | "fork" Mode string // Content 内容:inline 为展开的提示词,fork 为子 Agent 结果 Content string // AllowedTools inline 模式的工具限制列表(信息性) AllowedTools []string // Model 建议使用的模型(可空) Model string } // SkillEntryDesc 是用于工具描述的 Skill 精简信息. type SkillEntryDesc struct { Name string Description string WhenToUse string ArgumentHint string UserInvocable bool } // SkillTool 实现 tools.Tool 接口. // 通过 SkillExecutor 接口执行 Skill,与 engine 包解耦. type SkillTool struct { executor SkillExecutor } // NewSkillTool 创建 SkillTool 实例. // executor 初始为 nil,需通过 SetExecutor 注入(类似 AgentTool). func NewSkillTool() *SkillTool { return &SkillTool{} } // SetExecutor 注入 Skill 执行器. // 在 Engine 初始化完成后,由 SetupSkillTool 调用. func (t *SkillTool) SetExecutor(exec SkillExecutor) { t.executor = exec } // Name 返回工具名称. // 精妙之处(CLEVER): 工具名为 "Skill"(大写),与早期实现 保持一致, // 确保 prompt cache 的工具名 key 稳定. func (t *SkillTool) Name() string { return "Skill" } // Description 返回工具描述,包含可用 Skill 列表(动态生成). // // 升华改进(ELEVATED): 早期实现 用 1% token 预算截断描述. // 我们用固定 8000 字符上限,更简单且与 tokenizer 解耦. // 替代方案:动态计算 1% 预算(需要引入 tokenizer,增加复杂性). func (t *SkillTool) Description(ctx context.Context) string { base := `Execute a predefined skill (workflow). Skills are reusable prompt workflows stored in .flyto/skills/ or .claude/skills/. Skill{skill: "name"} expands the skill's prompt template in the current conversation (inline mode). Skills with context: fork run as an isolated sub-agent and return their result. Use the skill name exactly as shown below. Pass arguments via the args field (replaces $ARGUMENTS in the skill template).` if t.executor == nil { return base } entries := t.executor.ListSkillEntries() if len(entries) == 0 { return base } var sb strings.Builder sb.WriteString(base) sb.WriteString("\n\nAvailable skills:\n") const maxChars = 8000 for _, e := range entries { line := fmt.Sprintf("- %s", e.Name) if e.Description != "" { // 截断长描述 desc := e.Description if len(desc) > 200 { desc = desc[:197] + "..." } line += ": " + desc } if e.ArgumentHint != "" { line += " (args: " + e.ArgumentHint + ")" } line += "\n" if sb.Len()+len(line) > maxChars { sb.WriteString("...(more skills available)\n") break } sb.WriteString(line) } return sb.String() } // InputSchema 返回 JSON Schema 输入定义. func (t *SkillTool) InputSchema() json.RawMessage { return json.RawMessage(`{ "type": "object", "properties": { "skill": { "type": "string", "description": "The skill name. E.g., \"commit\", \"review-pr\", or \"discuss\"" }, "args": { "type": "string", "description": "Optional arguments passed to the skill (replaces $ARGUMENTS in the skill template)" } }, "required": ["skill"] }`) } // Metadata 返回工具元数据. func (t *SkillTool) Metadata() tools.Metadata { return tools.Metadata{ ConcurrencySafe: false, ReadOnly: false, PermissionClass: permission.PermClassGeneric, AuditOperation: "invoke", } } // Execute 执行 Skill 工具调用. // // 执行流程: // 1. 解析输入参数 // 2. 若 executor 未注入,返回提示信息(优雅降级) // 3. 调用 executor.InvokeSkill // 4. inline 模式:返回展开的提示词(LLM 接收并执行) // 5. fork 模式:返回子 Agent 的结果文本 func (t *SkillTool) Execute(ctx context.Context, input json.RawMessage, _ tools.ProgressFunc) (*tools.Result, error) { var in skillInput if err := json.Unmarshal(input, &in); err != nil { return &tools.Result{Output: fmt.Sprintf("invalid input: %v", err), IsError: true}, nil } if in.Skill == "" { return &tools.Result{Output: "skill name is required", IsError: true}, nil } // executor 未注入:优雅降级 if t.executor == nil { return &tools.Result{Output: fmt.Sprintf( "Skill %q could not be executed: SkillTool executor not configured. "+ "Call SetExecutor() or SetupSkillTool() on the engine.", in.Skill, )}, nil } result, err := t.executor.InvokeSkill(ctx, in.Skill, in.Args) if err != nil { return &tools.Result{Output: fmt.Sprintf("skill %q failed: %v", in.Skill, err), IsError: true}, nil } switch result.Mode { case "inline": // inline 模式:把展开的提示词作为工具结果返回. // LLM 读到此内容后,按提示词执行相应操作. // // 精妙之处(CLEVER): 工具结果即是"指令注入"--LLM 将工具结果视为来自系统的 // 可信内容,会忠实执行其中的指令.这比注入 user 消息更简洁: // 不需要修改消息历史,工具调用/结果的配对天然是对话的一部分. // 替代方案:注入 user 消息(需要修改 runLoop,实现更复杂). output := result.Content if len(result.AllowedTools) > 0 { // 在提示词末尾追加工具限制信息(信息性,LLM 参考) output += fmt.Sprintf("\n\n[Skill recommended tools: %s]", strings.Join(result.AllowedTools, ", ")) } if result.Model != "" { output += fmt.Sprintf("\n[Skill suggested model: %s]", result.Model) } return &tools.Result{Output: output}, nil case "fork": // fork 模式:子 Agent 已执行完毕,返回结果. return &tools.Result{Output: result.Content}, nil default: return &tools.Result{Output: result.Content}, nil } }