package engine // skill_def.go 实现 Skill 注册表和调用逻辑. // // 核心设计: // - SkillRegistry 绑定到 Engine 实例(非全局单例),支持 SaaS 多租户隔离 // - Register/RegisterBuiltin 双路注册:文件加载 + 代码内置 // - Invoke 统一入口:内部根据 Skill.Context 决定 inline 或 fork // - fork 深度限制(≤2):防止嵌套 Skill 爆炸 // - inline 执行:展开模板后直接返回提示词,调用方(SkillTool)将其作为工具结果 // - fork 执行:spawn SubAgent,同步等待,返回结果文本 // // 升华改进(ELEVATED): 早期实现 Skill 系统是进程全局的(getSkillToolCommands = memoize). // 我们把 SkillRegistry 作为 Engine 的成员字段-- // // HTTP API 模式:不同 workspace 的 Engine 实例各自拥有独立的 SkillRegistry, // Skill A 对用户甲可见,对用户乙不可见,天然多租户隔离. // // 替代方案:全局 sync.Map(早期方案做法,SaaS 场景无法隔离). import ( "context" "fmt" "os" "path/filepath" "sync" "git.flytoex.net/yuanwei/flyto-agent/pkg/tools/builtin" ) // SkillSpawner 是 SkillRegistry 执行 fork 类型 Skill 所需的最小接口. // // 升华改进(ELEVATED): 从 *Engine 具体类型提取为最小接口-- // SkillRegistry 不再持有整个 Engine 引用,只持有"能启动子Agent并运行"这一个能力. // 好处: // 1. 打破循环依赖:SkillRegistry ↛ Engine(单向:Engine → SkillRegistry) // 2. 测试可注入 mock,无需构造真实 Engine // 3. 未来可以替换实现(如远程 Agent,受限沙箱 Agent) // // 替代方案:<保留 *Engine>--否决:循环依赖,测试困难,职责错配. type SkillSpawner interface { // SpawnSkillAgent 创建并同步运行一个 SubAgent,返回其结果文本. // cfg 描述 SubAgent 的工具限制和模型配置,prompt 是传入的提示词. SpawnSkillAgent(ctx context.Context, cfg *SubAgentConfig, prompt string) (string, error) } // MaxSkillForkDepth 最大 Skill fork 嵌套深度. // // 精妙之处(CLEVER): 限制为 2(而非 1)允许一次合理的嵌套: // // review-pr Skill (depth=1) → 调用 run-tests Skill (depth=2) → 不允许再 fork // // 深度 0 = 顶层 LLM 直接调用 SkillTool. // 替代方案:<限制为 1(完全禁止嵌套)> - 否决:会禁止 "review-pr 调 run-tests" 这类合理场景. const MaxSkillForkDepth = 2 // skillDepthKey 是 context 中存储 Skill 嵌套深度的 key. // 使用私有类型避免与其他包的 context key 冲突. type skillDepthKey struct{} // skillDepthFromCtx 从 context 中读取当前 Skill 嵌套深度(默认 0). func skillDepthFromCtx(ctx context.Context) int { d, _ := ctx.Value(skillDepthKey{}).(int) return d } // withSkillDepth 返回携带指定深度的新 context. func withSkillDepth(ctx context.Context, d int) context.Context { return context.WithValue(ctx, skillDepthKey{}, d) } // SkillInvokeResult 是 engine 内部 Invoke 方法的返回结果. // 对外(SkillTool)通过 builtin.SkillResult 暴露(见 InvokeSkill 方法). // // 精妙之处(CLEVER): 保留引擎内部类型 + 暴露 builtin 兼容类型-- // Go SDK 用户调用 registry.Invoke() 得到完整类型(含 ExecutionContext 枚举), // SkillTool 调用 registry.InvokeSkill() 得到 builtin.SkillResult(接口兼容). // 两个 API 并存,不同场景各取所需. // 替代方案:直接暴露 builtin.SkillResult(损失 ExecutionContext 类型安全性). type SkillInvokeResult struct { Mode ExecutionContext // "inline" | "fork" Content string // 展开的提示词(inline)或子 Agent 结果(fork) AllowedTools []string // 工具限制(来自 Skill.AllowedTools) Model string // 模型建议(来自 Skill.Model) } // SkillRegistry 管理所有已注册的 Skill. // // 线程安全(RWMutex). // 通过 Engine.skillRegistry 访问,不暴露为全局变量. type SkillRegistry struct { mu sync.RWMutex skills map[string]*Skill // name → *Skill spawner SkillSpawner // 用于 fork 执行(原为 engine *Engine) // agentRegistry resolves Skill.AgentType → AgentDefinition for fork Skills. // Optional: nil (unset) means skill.AgentType is ignored even when non-empty // (SDK test path). Engine.New wires the production instance after // newSkillRegistry via SetAgentRegistry; this keeps 16 test call sites of // newSkillRegistry(nil) untouched. // // agentRegistry 负责把 Skill.AgentType 解析为 AgentDefinition, 服务 fork 类 // Skill. 可选字段: nil (未注入) 时即便 skill.AgentType 非空也忽略, 用于 SDK // 单元测试路径. 生产注入由 Engine.New 在 newSkillRegistry 之后经 SetAgentRegistry // 完成, 这样 16 处 newSkillRegistry(nil) 的测试调用点不需要同步修改. agentRegistry *AgentRegistry // observer emits skill_invoked / skill_fork_unknown_agent_type diagnostic // events. Optional: nil (unset) disables emission. Production path sets this // to eng.observer, which is never nil (defaults to NoopObserver). // // observer 负责发 skill_invoked / skill_fork_unknown_agent_type 诊断事件. // 可选字段: nil 关闭发射. 生产路径经 SetObserver 注入 eng.observer, // 后者在 engine.New 阶段保证非 nil (未配置时默认 NoopObserver). observer EventObserver // parentToolNames returns the name list of tools visible to the parent // Engine at the time of a fork Skill invocation. Required for the // AgentRegistry.ResolveToolset four-layer filter (parent tools → def // AllowedTools whitelist → Background whitelist → DisallowedTools + // globalDisallowed blacklist) that is the core of the "fork SubAgent // really behaves as the declared Agent type" promise. Optional: nil // (unset) skips the ResolveToolset path — acceptable for SDK unit tests // with newSkillRegistry(nil). // // parentToolNames 返回父 Engine 在 fork Skill 调用时刻可见的工具名称列表. // AgentRegistry.ResolveToolset 的四层过滤 (父工具 → def AllowedTools 白名单 // → Background 白名单 → DisallowedTools + globalDisallowed 黑名单) 需要这个 // 输入, 而这正是 "fork 出的 SubAgent 真正表现成所声明的 Agent 类型" 承诺的 // 核心. 可选字段: nil (未注入) 跳过 ResolveToolset 路径 -- 适用于 // newSkillRegistry(nil) 的 SDK 单元测试. parentToolNames func() []string } // newSkillRegistry 创建空的注册表,绑定到指定 spawner. // spawner 为 nil 时,fork 类型 Skill 降级为 inline 执行. func newSkillRegistry(spawner SkillSpawner) *SkillRegistry { return &SkillRegistry{ skills: make(map[string]*Skill), spawner: spawner, } } // SetAgentRegistry wires the AgentRegistry used by invokeFork to resolve // skill.AgentType. Idempotent; nil clears. Engine.New calls this in the // post-construction wiring block to avoid breaking the 16 existing // newSkillRegistry(nil) test call sites with a signature change. // // SetAgentRegistry 注入供 invokeFork 解析 skill.AgentType 使用的 AgentRegistry. // 幂等, 传 nil 清空. Engine.New 在后置回填块里调用, 避免改动 newSkillRegistry 签名 // 打破现有 16 处 newSkillRegistry(nil) 测试调用. func (r *SkillRegistry) SetAgentRegistry(ar *AgentRegistry) { r.mu.Lock() defer r.mu.Unlock() r.agentRegistry = ar } // SetObserver wires the EventObserver that receives skill_invoked / // skill_fork_unknown_agent_type events. Idempotent; nil disables emission. // // SetObserver 注入接收 skill_invoked / skill_fork_unknown_agent_type 事件的 // EventObserver. 幂等, 传 nil 关闭发射. func (r *SkillRegistry) SetObserver(o EventObserver) { r.mu.Lock() defer r.mu.Unlock() r.observer = o } // SetParentToolNames wires the provider function for parent tool name list, // used by invokeFork to call AgentRegistry.ResolveToolset. Engine.New injects // `func() []string { return eng.tools.Names() }`. nil clears. // // SetParentToolNames 注入父工具名列表的 provider, 供 invokeFork 调用 // AgentRegistry.ResolveToolset. Engine.New 传入 // `func() []string { return eng.tools.Names() }`. 传 nil 清空. func (r *SkillRegistry) SetParentToolNames(f func() []string) { r.mu.Lock() defer r.mu.Unlock() r.parentToolNames = f } // Register 注册一个 Skill.若已存在同名 Skill,返回错误. // 用于文件加载的 Skill(允许冲突报错,提示用户命名重复). func (r *SkillRegistry) Register(s *Skill) error { r.mu.Lock() defer r.mu.Unlock() if _, exists := r.skills[s.Name]; exists { return fmt.Errorf("skill %q already registered", s.Name) } r.skills[s.Name] = s return nil } // RegisterBuiltin 注册一个内置 Skill(无条件覆盖同名). // 用于代码内置的 Skill(RegisterBuiltin 模式). // // 精妙之处(CLEVER): 内置 Skill 总是胜过文件 Skill(后注册 vs 先加载)-- // 这与 Plugin.RegisterBuiltin 的语义一致:内置实现是权威定义, // 文件 Skill 是用户自定义扩展,不能覆盖核心功能. // 替代方案:文件优先(会让 discuss.md 覆盖内置 discuss Skill,难以调试). func (r *SkillRegistry) RegisterBuiltin(s *Skill) { if s.Source == "" { s.Source = "bundled" } r.mu.Lock() r.skills[s.Name] = s r.mu.Unlock() } // RegisterAll 批量注册 Skill 列表(覆盖同名,后加载优先). // 用于 ScanSkillsDir 返回的结果批量加载. func (r *SkillRegistry) RegisterAll(skills []*Skill) { r.mu.Lock() defer r.mu.Unlock() for _, s := range skills { r.skills[s.Name] = s } } // Get 按名称查找 Skill. func (r *SkillRegistry) Get(name string) (*Skill, bool) { r.mu.RLock() defer r.mu.RUnlock() s, ok := r.skills[name] return s, ok } // List 返回所有已注册的 Skill 列表. // 可选传入过滤函数(返回 false 则排除). func (r *SkillRegistry) List(filters ...func(*Skill) bool) []*Skill { r.mu.RLock() defer r.mu.RUnlock() result := make([]*Skill, 0, len(r.skills)) for _, s := range r.skills { include := true for _, f := range filters { if !f(s) { include = false break } } if include { result = append(result, s) } } return result } // ListForCwd 返回在给定工作目录下激活的 Skill 列表. // // 升华改进(ELEVATED): 14.P1-A paths 激活-- // 过滤出 Skill.Paths 匹配当前 CWD 的 Skill,用于系统提示词生成和 UI 补全. // 与 List() 不同,此方法按 CWD 激活语义过滤,而非返回全量注册表. // Paths 为空的 Skill 视为"全局激活",始终包含在结果中. // // 精妙之处(CLEVER): 委托给 Skill.MatchesCwd,逻辑单一责任-- // 激活判断逻辑只在 Skill.MatchesCwd 里,ListForCwd 只做 filter, // 测试可以单独测 MatchesCwd 而无需构造 Registry. // 替代方案:在 ListForCwd 内部内联路径匹配逻辑(逻辑分散,难维护). func (r *SkillRegistry) ListForCwd(cwd string, filters ...func(*Skill) bool) []*Skill { r.mu.RLock() defer r.mu.RUnlock() result := make([]*Skill, 0, len(r.skills)) for _, s := range r.skills { // paths 激活过滤 if !s.MatchesCwd(cwd) { continue } // 附加调用方过滤器 include := true for _, f := range filters { if !f(s) { include = false break } } if include { result = append(result, s) } } return result } // Invoke 执行指定 Skill. // // 根据 Skill.Context 决定执行方式: // - ExecutionContextInline(默认):展开提示词模板,直接返回文本 // - ExecutionContextFork:spawn SubAgent,同步运行,返回结果 // // 参数: // - ctx: 调用上下文(包含 Skill 嵌套深度) // - name: Skill 名称 // - args: 用户传入的参数字符串(替换 $ARGUMENTS) // - sessionID: 当前会话 ID(替换 ${FLYTO_SESSION_ID}) func (r *SkillRegistry) Invoke(ctx context.Context, name, args, sessionID string) (*SkillInvokeResult, error) { s, ok := r.Get(name) if !ok { return nil, fmt.Errorf("skill %q not found", name) } execCtx := s.Context if execCtx == "" { execCtx = ExecutionContextInline } // Emit skill_invoked before dispatch so operators / audit pipelines see // *every* Skill activation including the ones that error out mid-fork. // file_path is kept as "" when empty (NOT omitted) so consumers can tell // an in-memory skill ("") apart from one loaded from disk; this matches // the FilePath godoc "空 = 内存注册". // // 在 dispatch 之前发 skill_invoked, 保证 operator/审计管道看到**每一次** Skill // 激活 -- 包括 fork 中途报错的情形. file_path 空串**显式保留** (不 omitempty), // 消费方据此区分 in-memory skill ("") 和磁盘加载的 skill, 对齐 FilePath godoc // "空 = 内存注册" 承诺. r.emitSkillInvoked(s, execCtx, args) switch execCtx { case ExecutionContextFork: return r.invokeFork(ctx, s, args, sessionID) default: return r.invokeInline(s, args, sessionID) } } // emitSkillInvoked emits the skill_invoked observer event. No-op when observer // is not wired (SDK unit tests with newSkillRegistry(nil)). // // emitSkillInvoked 发 skill_invoked 观测事件. observer 未注入时 no-op, 服务 // newSkillRegistry(nil) 的 SDK 单元测试路径. func (r *SkillRegistry) emitSkillInvoked(s *Skill, execCtx ExecutionContext, args string) { r.mu.RLock() obs := r.observer r.mu.RUnlock() if obs == nil { return } obs.Event("skill_invoked", map[string]any{ "name": s.Name, "file_path": s.FilePath, "version": s.Version, "context_mode": string(execCtx), "args_len": len(args), }) } // invokeInline 内联执行:展开模板,返回提示词文本. func (r *SkillRegistry) invokeInline(s *Skill, args, sessionID string) (*SkillInvokeResult, error) { expanded := s.ExpandPrompt(args, sessionID) return &SkillInvokeResult{ Mode: ExecutionContextInline, Content: expanded, AllowedTools: s.AllowedTools, Model: s.Model, }, nil } // invokeFork fork 执行:spawn SubAgent,同步等待结果. // // 深度限制:若当前深度已达 MaxSkillForkDepth,降级为 inline 执行(不报错). // 深度通过 context 传播:SubAgent 运行时携带 depth+1 的 context, // 若 SubAgent 内部再触发 SkillTool(fork),会再次检查深度. // // 精妙之处(CLEVER): 深度通过 context.Value 传递,不需要修改任何接口-- // SpawnSubAgent,RunSync 都不知道 Skill 深度的存在, // 只有 SkillRegistry.invokeFork 在两端(读取 + 写入)感知深度. // 这是"背包信息"(hitchhiker pattern)的典型用法. func (r *SkillRegistry) invokeFork(ctx context.Context, s *Skill, args, sessionID string) (*SkillInvokeResult, error) { depth := skillDepthFromCtx(ctx) if depth >= MaxSkillForkDepth { // 降级为 inline,不返回错误(避免 fork 嵌套限制破坏用户体验) // // 升华改进(ELEVATED): 早期实现 无深度限制(无限嵌套可能耗尽 token 预算). // 我们降级为 inline 而非报错--用户体验更好(Skill 仍然执行,只是上下文共享). // 替代方案:返回 error(调用中断,用户体验差). return r.invokeInline(s, args, sessionID) } if r.spawner == nil { return nil, fmt.Errorf("skill %q requires fork execution but SkillRegistry has no spawner", s.Name) } expanded := s.ExpandPrompt(args, sessionID) // 解析工具白名单 var allowedMap map[string]bool if len(s.AllowedTools) > 0 { allowedMap = make(map[string]bool, len(s.AllowedTools)) for _, t := range s.AllowedTools { allowedMap[t] = true } } cfg := &SubAgentConfig{ Description: fmt.Sprintf("Skill: %s", s.Name), Model: s.Model, AllowedTools: allowedMap, MaxTurns: s.MaxTurns, } // Wire skill.AgentType → full AgentDefinition merge. The godoc promise // "fork 模式下使用的 Agent 类型" requires the spawned SubAgent to actually // BEHAVE as the declared Agent type, not merely carry its name. Concretely: // // (a) cfg.AllowedTools ← ResolveToolset(def, parentTools) four-layer filter // (parent ∩ def.AllowedTools, Background narrow, // minus def.DisallowedTools ∪ globalDisallowed). // MCP tools auto-pass the whitelist layer. // (b) DisallowedTools ← applied as layer-4 inside ResolveToolset (random // verification: DisallowedTools keywords in def // remove from workingSet). No separate cfg field. // (c) cfg.Model ← fallback to def.Model when skill.Model == "" // (d) cfg.MaxTurns ← fallback to def.MaxTurns when skill.MaxTurns == 0 // (e) AllowedSubAgentTypes ← = def.AllowedSubAgentTypes // (f) When skill itself declared AllowedTools, INTERSECT with ResolveToolset // result — skill is authoritative for "what I want", but def is // authoritative for "what this Agent type is allowed to do". Skill // cannot escape def constraints by declaring extra tools; the // intersection is the security invariant for "agent: Explore always // means Explore-bounded". // // Misses: if AgentType is typo'd (registry.Get returns !ok), emit // skill_fork_unknown_agent_type observer event and fall back to the // empty-AgentType branch (no resolution, skill's own fields used as-is). // Silent fallback would hide typos; hard error would break UX. // // Edge case: parentToolNames getter is nil (SDK unit tests with // newSkillRegistry(nil)). ResolveToolset needs parentTools as input, so // skip the (a)/(b)/(f) merge when the getter is unwired. (c)(d)(e) still // apply — they don't depend on parent tools. // // Wire skill.AgentType → 完整合并 AgentDefinition. godoc 承诺 "fork 模式下 // 使用的 Agent 类型" 要求 fork 出的 SubAgent 真正**表现成**所声明的 Agent // 类型, 不只是挂一个名字. 具体兑现: // // (a) cfg.AllowedTools ← ResolveToolset(def, parentTools) 四层过滤 // (父工具 ∩ def.AllowedTools → Background 收窄 → // 去 def.DisallowedTools ∪ globalDisallowed). MCP // 工具在白名单层自动通过. // (b) DisallowedTools ← ResolveToolset 第四层已处理, cfg 无独立字段. // (c) cfg.Model ← skill.Model 为 "" 时 fallback 到 def.Model // (d) cfg.MaxTurns ← skill.MaxTurns 为 0 时 fallback 到 def.MaxTurns // (e) AllowedSubAgentTypes ← = def.AllowedSubAgentTypes // (f) skill 自己声明了 AllowedTools 时, 与 ResolveToolset 结果取交集 -- // skill 对 "我要什么" 权威, def 对 "这个 Agent 类型允许什么" 权威. // skill 不能通过声明额外工具绕过 def 的约束, 交集是 // "agent: Explore 始终意味着 Explore-bounded" 的安全不变量. // // 未命中: AgentType 拼写错 (registry.Get !ok) 时发 // skill_fork_unknown_agent_type observer 事件并 fallback 到空 AgentType // 分支 (不解析, skill 自身字段直接用). 静默 fallback 会隐藏拼写错误, // 硬报错又会破坏体验. // // 边界: parentToolNames getter 为 nil (newSkillRegistry(nil) 单测路径) 时, // ResolveToolset 需要 parentTools 输入, 跳过 (a)(b)(f). (c)(d)(e) 仍生效, // 它们不依赖父工具. if s.AgentType != "" { r.mu.RLock() ar := r.agentRegistry obs := r.observer parentToolsFn := r.parentToolNames r.mu.RUnlock() if ar != nil { if def, ok := ar.Get(s.AgentType); ok { // (a)(b)(f) 四层过滤 + skill 声明则取交集. if parentToolsFn != nil { resolved := ar.ResolveToolset(def, parentToolsFn()) resolvedMap := make(map[string]bool, len(resolved)) for _, n := range resolved { resolvedMap[n] = true } if cfg.AllowedTools == nil { cfg.AllowedTools = resolvedMap } else { intersected := make(map[string]bool, len(cfg.AllowedTools)) for name := range cfg.AllowedTools { if resolvedMap[name] { intersected[name] = true } } cfg.AllowedTools = intersected } } // (c) Model fallback if cfg.Model == "" { cfg.Model = def.Model } // (d) MaxTurns fallback if cfg.MaxTurns == 0 { cfg.MaxTurns = def.MaxTurns } // (e) AllowedSubAgentTypes cfg.AllowedSubAgentTypes = def.AllowedSubAgentTypes } else if obs != nil { obs.Event("skill_fork_unknown_agent_type", map[string]any{ "skill_name": s.Name, "agent_type": s.AgentType, }) } } } // 兜底默认 10. 放在 AgentType 分支之后保证 def.MaxTurns=0 时仍回落到 10, // 而不是被 def.MaxTurns=0 覆盖. if cfg.MaxTurns == 0 { cfg.MaxTurns = 10 } // 将 depth+1 注入 context,传递给子 Agent 的运行 childCtx := withSkillDepth(ctx, depth+1) result, err := r.spawner.SpawnSkillAgent(childCtx, cfg, expanded) if err != nil { return nil, fmt.Errorf("skill %q fork execution failed: %w", s.Name, err) } return &SkillInvokeResult{ Mode: ExecutionContextFork, Content: result, }, nil } // InvokeSkill 实现 builtin.SkillExecutor 接口. // 供 SkillTool 通过接口调用(避免 builtin 包直接依赖 engine 包). // // 精妙之处(CLEVER): 将引擎内部的 *SkillInvokeResult 转换为 *builtin.SkillResult-- // 保持引擎 API 类型安全(ExecutionContext 枚举)的同时满足 builtin 包接口要求. // 这与 agentExecutor 把 engine.SubAgent 结果转换为字符串返回的做法一致. func (r *SkillRegistry) InvokeSkill(ctx context.Context, name, args string) (*builtin.SkillResult, error) { // 从 context 取 session ID(若有) sessionID, _ := ctx.Value(ctxKeySessionID{}).(string) res, err := r.Invoke(ctx, name, args, sessionID) if err != nil { return nil, err } return &builtin.SkillResult{ Mode: string(res.Mode), Content: res.Content, AllowedTools: res.AllowedTools, Model: res.Model, }, nil } // ListSkillEntries 实现 builtin.SkillExecutor 接口. func (r *SkillRegistry) ListSkillEntries() []*builtin.SkillEntryDesc { all := r.List() entries := make([]*builtin.SkillEntryDesc, 0, len(all)) for _, s := range all { entries = append(entries, &builtin.SkillEntryDesc{ Name: s.Name, Description: s.Description, WhenToUse: s.WhenToUse, ArgumentHint: s.ArgumentHint, UserInvocable: s.UserInvocable, }) } return entries } // ctxKeySessionID 是从 context 取会话 ID 的 key. type ctxKeySessionID struct{} // SetupSkillTool 为 Engine 扫描并加载文件系统 Skill,绑定 SkillTool 执行器. // // 默认扫描路径(按优先级,后加载覆盖先加载): // 1. ~/.flyto/skills/ 和 ~/.claude/skills/ (用户级) // 2. /.flyto/skills/ 和 /.claude/skills/ (项目级) // 3. extraDirs (额外目录) // // 返回 SkillRegistry 供外部继续注册内置 Skill. func SetupSkillTool(engine *Engine, extraDirs []string) *SkillRegistry { r := engine.skillRegistry if r == nil { return nil } type dirWithSource struct { path string source string } var dirs []dirWithSource // 1. 用户目录 if home, err := os.UserHomeDir(); err == nil { dirs = append(dirs, dirWithSource{filepath.Join(home, ".flyto", "skills"), "user"}, dirWithSource{filepath.Join(home, ".claude", "skills"), "user"}, ) } // 2. 项目目录 if engine.cfg != nil && engine.cfg.Cwd != "" { dirs = append(dirs, dirWithSource{filepath.Join(engine.cfg.Cwd, ".flyto", "skills"), "project"}, dirWithSource{filepath.Join(engine.cfg.Cwd, ".claude", "skills"), "project"}, ) } // 3. 额外目录 for _, d := range extraDirs { dirs = append(dirs, dirWithSource{d, "project"}) } // 扫描所有目录(后加载覆盖先加载) for _, d := range dirs { skills, err := ScanSkillsDir(d.path, d.source) if err != nil { continue } r.RegisterAll(skills) } // 绑定 SkillTool 执行器(与 SetupAgentExecutor 同样的类型断言模式) if skillTool, ok := engine.tools.Get("Skill"); ok { if st, ok := skillTool.(*builtin.SkillTool); ok { st.SetExecutor(r) } } return r }