// Package skills 实现 M5 的可复用工作流能力:一个 Skill 是比 Tool 更结构化、 // 且可持久化的「高阶工作流」——一组带提示词的步骤(含依赖关系),配以基础 // 提示词、引用与约束(DevDocs/design/09 §0/§3/§6)。 // // 包内五件套: // // - Skill:实现 pkg/plugins/sdk.Skill(SkillSpec) 的具象内存模型。 // - Store:把技能定义持久化进 SQLite 的 skills 表。 // - Loader:从 YAML 加载定义(单流 / 单文件 / configs/skills 目录),并做 // 加载期校验(依赖缺失、依赖环、allowed/disallowed 冲突等,见 Validate)。 // - Runner:把技能展开成可执行工作流(Expand),并**真执行**(Run/RunSkill): // 步骤拓扑调度 + 无依赖步骤并行(上限 2,硬上限 3)+ guardrail 强制点。 // - Register:把技能作为 KindSkill 能力发布进全局能力注册表 // (internal/capability/registry)。 // // 能力收缩(design/09 §6.4-3)见 surface.go:技能轮次可用工具 = allowed-tools // ∩ 安全闸 ∩ 全局注册表 − disallowed-tools,**任何情况下不因技能放宽**。 package skills import ( "errors" "fmt" "io" "log/slog" "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" "git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk" ) // 本包的错误哨兵。调用方一律用 errors.Is 分类,绝不靠字符串匹配。 var ( // ErrInvalidSkill 表示技能为 nil 或名字为空。 ErrInvalidSkill = errors.New("skills: invalid skill") // ErrNotFound 由 Store 在目标技能不存在时返回。 ErrNotFound = errors.New("skills: skill not found") // ErrNilRegistrar 由 Register/NewRunner 在未提供注册表时返回。 ErrNilRegistrar = errors.New("skills: nil registrar") // ErrNilDB 由 NewStore 在未提供 *sql.DB 时返回。 ErrNilDB = errors.New("skills: nil db") // ErrStepDuplicate 表示步骤名重复(依赖图会因此歧义,故拒绝)。 ErrStepDuplicate = errors.New("skills: duplicate step name") // ErrStepDependsMissing 表示 steps[].depends 指向了不存在的步骤。 ErrStepDependsMissing = errors.New("skills: step depends on unknown step") // ErrStepCycle 表示步骤依赖成环(加载期就拒,运行期绝不挂死)。 ErrStepCycle = errors.New("skills: step dependency cycle") // ErrToolConflict 表示同一工具同时出现在 allowed-tools 与 disallowed-tools // (自相矛盾的声明,拒绝而不是猜哪边赢)。 ErrToolConflict = errors.New("skills: tool both allowed and disallowed") // ErrGuardrailDuplicate 表示 guardrail id 重复。 ErrGuardrailDuplicate = errors.New("skills: duplicate guardrail id") // ErrNilGate 表示执行技能时没给安全闸 —— 缺闸即拒绝(只收不放,fail-closed)。 ErrNilGate = errors.New("skills: nil tool gate") // ErrNoExecutor 表示执行技能时没给步骤执行器。 ErrNoExecutor = errors.New("skills: nil step executor") // ErrStepFailed 表示某一步执行失败(**不静默跳过**,错误里带步骤名)。 ErrStepFailed = errors.New("skills: step failed") // ErrGuardrailViolation 表示 enforced guardrail 被违反,技能已中止; // 错误里如实带上「哪条 guardrail、哪一步」。 ErrGuardrailViolation = errors.New("skills: guardrail violated") // ErrGuardrailCheck 表示 guardrail 校验器本身失败(与「违规」区分开, // 绝不把校验失败当成通过)。 ErrGuardrailCheck = errors.New("skills: guardrail check failed") ) // Skill 是技能的可持久化具象形态(对应 pkg/plugins/sdk.Skill):一组带依赖的 // 有序步骤 + 基础提示词 + 引用 + 约束,外加 design/09 §6.2 定稿的新增字段 // (版本、调用三态、触发词/路径、工具上限、上下文模式、模型覆盖、元数据)。 // // Skill 构造后不再变化;所有 getter 都返回切片/映射的副本,因此 *Skill 可以按 // sdk.Skill 的契约要求并发使用。 type Skill struct { name string description string prompt string steps []sdk.Step refs []sdk.Ref guardrails []sdk.Guardrail spec Spec } // 编译期断言:*Skill 同时满足基础 sdk.Skill 与完整的 sdk.SkillSpec。 var ( _ sdk.Skill = (*Skill)(nil) _ sdk.SkillSpec = (*Skill)(nil) ) // Spec 是 design/09 §6.2 定稿的新增字段集合。零值即「全部取缺省」: // version=1、invocation=both、context=inline,其余为空。 // // 它同时是 Store 里 spec_json 列的 JSON 形态,因此字段名带 json tag。 type Spec struct { // Version 是定义版本(形状迁移用),非正数取缺省 1。 Version int `json:"version,omitempty"` // WhenToUse 是「何时使用」补充说明,与 Description 一起进清单并计入预算。 WhenToUse string `json:"whenToUse,omitempty"` // Invocation 是调用三态;空取缺省 both。 Invocation sdk.Invocation `json:"invocation,omitempty"` // Triggers 是触发词。 Triggers []string `json:"triggers,omitempty"` // Paths 是 glob;仅当工作文件命中时自动激活。 Paths []string `json:"paths,omitempty"` // AllowedTools 是能力**上限**(不是免审批名单),见 design/09 §6.4-3。 AllowedTools []string `json:"allowedTools,omitempty"` // DisallowedTools 从上限里剔除。 DisallowedTools []string `json:"disallowedTools,omitempty"` // Context 是上下文模式(YAML 键 `context`);空取缺省 inline。 Context sdk.ContextMode `json:"context,omitempty"` // Agent 是 context=fork 时使用的子代理类型。 Agent string `json:"agent,omitempty"` // Model 是可选模型覆盖;per-role 路由优先于它(design/04 §3)。 Model string `json:"model,omitempty"` // Effort 是可选思考档位覆盖。 Effort string `json:"effort,omitempty"` // Metadata 是自由 KV,宿主不解释。 Metadata map[string]string `json:"metadata,omitempty"` } // Option 定制 Skill 的构造。目前只有 WithSpec 一条 —— 新增字段整体走 Spec, // 避免 11 个单字段 option 把构造面撑爆。 type Option func(*Skill) // WithSpec 写入 design/09 §6.2 的新增字段(切片/映射会被复制)。 func WithSpec(spec Spec) Option { return func(s *Skill) { s.spec = cloneSpec(spec) } } // New 构造一个 Skill。名字为空时返回 ErrInvalidSkill。步骤、引用、约束与 Spec // 都会被复制,因此调用方之后改动入参不会影响已构造的技能。 func New(name, description, prompt string, steps []sdk.Step, refs []sdk.Ref, guardrails []sdk.Guardrail, opts ...Option) (*Skill, error) { if strings.TrimSpace(name) == "" { return nil, fmt.Errorf("skills: new: %w", ErrInvalidSkill) } s := &Skill{ name: name, description: description, prompt: prompt, steps: cloneSteps(steps), refs: cloneRefs(refs), guardrails: cloneGuardrails(guardrails), } for _, opt := range opts { if opt != nil { opt(s) } } s.spec = normalizeSpec(s.spec) return s, nil } // Name 返回技能的唯一标识。 func (s *Skill) Name() string { return s.name } // Description 说明技能做什么;模型据此挑选技能。 func (s *Skill) Description() string { return s.description } // Prompt 返回技能的基础系统提示词。 func (s *Skill) Prompt() string { return s.prompt } // Steps 返回有序工作流步骤的副本。 func (s *Skill) Steps() []sdk.Step { return cloneSteps(s.steps) } // Refs 返回技能可引用素材的副本。 func (s *Skill) Refs() []sdk.Ref { return cloneRefs(s.refs) } // Guardrails 返回技能约束的副本。 func (s *Skill) Guardrails() []sdk.Guardrail { return cloneGuardrails(s.guardrails) } // Spec 返回新增字段集合的副本(含归一化后的缺省值)。 func (s *Skill) Spec() Spec { return cloneSpec(s.spec) } // Version 返回定义版本(缺省 1)。 func (s *Skill) Version() int { return s.spec.Version } // WhenToUse 返回「何时使用」补充说明。 func (s *Skill) WhenToUse() string { return s.spec.WhenToUse } // Invocation 返回调用三态(缺省 both)。 func (s *Skill) Invocation() sdk.Invocation { return s.spec.Invocation } // Triggers 返回触发词副本。 func (s *Skill) Triggers() []string { return append([]string(nil), s.spec.Triggers...) } // Paths 返回 glob 副本。 func (s *Skill) Paths() []string { return append([]string(nil), s.spec.Paths...) } // AllowedTools 返回能力上限副本(空表示不限,仍受安全闸约束)。 func (s *Skill) AllowedTools() []string { return append([]string(nil), s.spec.AllowedTools...) } // DisallowedTools 返回剔除名单副本。 func (s *Skill) DisallowedTools() []string { return append([]string(nil), s.spec.DisallowedTools...) } // ContextMode 返回上下文模式(缺省 inline)。 func (s *Skill) ContextMode() sdk.ContextMode { return s.spec.Context } // Agent 返回 context=fork 时使用的子代理类型。 func (s *Skill) Agent() string { return s.spec.Agent } // Model 返回可选模型覆盖。 func (s *Skill) Model() string { return s.spec.Model } // Effort 返回可选思考档位覆盖。 func (s *Skill) Effort() string { return s.spec.Effort } // Metadata 返回自由 KV 的副本。 func (s *Skill) Metadata() map[string]string { return cloneMetadata(s.spec.Metadata) } // normalizeSpec 补上文档规定的缺省值,并把 nil 切片/映射留成 nil(空 == 未声明)。 func normalizeSpec(spec Spec) Spec { if spec.Version <= 0 { spec.Version = sdk.DefaultSkillVersion } spec.Invocation = spec.Invocation.OrDefault() spec.Context = spec.Context.OrDefault() return spec } func cloneSpec(spec Spec) Spec { out := spec out.Triggers = append([]string(nil), spec.Triggers...) out.Paths = append([]string(nil), spec.Paths...) out.AllowedTools = append([]string(nil), spec.AllowedTools...) out.DisallowedTools = append([]string(nil), spec.DisallowedTools...) out.Metadata = cloneMetadata(spec.Metadata) return out } func cloneMetadata(in map[string]string) map[string]string { if len(in) == 0 { return nil } out := make(map[string]string, len(in)) for k, v := range in { out[k] = v } return out } func cloneSteps(in []sdk.Step) []sdk.Step { if len(in) == 0 { return nil } out := make([]sdk.Step, len(in)) for i, st := range in { out[i] = sdk.Step{ Name: st.Name, Prompt: st.Prompt, Depends: append([]string(nil), st.Depends...), } } return out } func cloneRefs(in []sdk.Ref) []sdk.Ref { if len(in) == 0 { return nil } out := make([]sdk.Ref, len(in)) copy(out, in) return out } func cloneGuardrails(in []sdk.Guardrail) []sdk.Guardrail { if len(in) == 0 { return nil } out := make([]sdk.Guardrail, len(in)) copy(out, in) return out } // sourcedLog 返回带来源标签(source=capability/skills)的日志器;l 为 nil 时 // 换成丢弃输出的日志器,让叶子调用方可以安全传 nil。 func sourcedLog(l *slog.Logger) *slog.Logger { if l == nil { l = slog.New(slog.NewTextHandler(io.Discard, nil)) } return logger.WithSource(l, "capability/skills") }