- 加载器/校验:目录式 configs/skills/<id>/skill.yaml(兼容旧单文件 <id>.yaml,先目录后文件); 新字段 version/when-to-use/invocation(三态)/triggers/paths/allowed-tools/disallowed-tools/ context(ine|fork)/agent/model/effort/metadata;**依赖环 ⇒ 加载期拒**(运行期绝不挂死); depends 指向不存在步骤 ⇒ 拒;同一工具同时在 allowed 与 disallowed ⇒ 拒;错误可分类(哨兵 + %w) - Runner.Run 真执行(替换 ErrRunPending):步骤拓扑(无依赖可并行、上限 2)+ guardrail 强制点 (enforced 违反 ⇒ 中止并如实报「哪条、哪一步」;否则 WARN 继续)+ 失败不静默跳过 + ctx 取消传播 - 🔴 能力收缩只收不放:该轮可用工具 = allowed-tools ∩ 安全闸 ∩ 注册表,再减 disallowed-tools - 另有渐进披露相关的 listing/surface/activation(清单投影与激活判定) ⚠️ 未接线:本片是库与执行器,尚未把技能清单接进模型面(见 report 的分阶段)。
280 lines
11 KiB
Go
280 lines
11 KiB
Go
// 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")
|
||
}
|