- 加载器/校验:目录式 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 的分阶段)。
320 lines
12 KiB
Go
320 lines
12 KiB
Go
package skills
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"log/slog"
|
||
"sort"
|
||
"strings"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
|
||
)
|
||
|
||
// Workflow 是一个展开后的技能:基础系统提示词 + 有序步骤 + 引用 + 约束,
|
||
// 可以直接交给执行侧。Runner.Expand 只做展开(不改数据),真执行走 RunSkill。
|
||
type Workflow struct {
|
||
Name string
|
||
Description string
|
||
Prompt string
|
||
Steps []sdk.Step
|
||
Refs []sdk.Ref
|
||
Guardrails []sdk.Guardrail
|
||
// Spec 是 design/09 §6.2 定稿的新增字段(含归一化缺省值)。
|
||
Spec Spec
|
||
}
|
||
|
||
// GuardrailFinalSubject 是收尾全量复核时传给 GuardrailChecker 的 subject:它表示
|
||
// 「本次校验的对象是**全部步骤的累计输出**」,而不是某一步。步骤名不可能取到这个
|
||
// 值(它以 $ 开头,且步骤名还会被 Validate 拒空)。
|
||
const GuardrailFinalSubject = "$final"
|
||
|
||
// StepRequest 是交给 StepExecutor 的一次步骤执行请求。每步 = 一次模型调用
|
||
// (design/09 §6.4-2),本结构把该步需要的全部信息一次给全,执行侧不必回读技能。
|
||
type StepRequest struct {
|
||
// Skill 是技能名。
|
||
Skill string
|
||
// Step 是当前步骤(含它自己的 prompt 与依赖声明)。
|
||
Step sdk.Step
|
||
// Prompt 是拼好的指令:技能基础提示词 + 本步提示词。
|
||
Prompt string
|
||
// Depends 是**本步依赖的**前序步骤输出(键为步骤名)。只有依赖它的步骤才拿得到,
|
||
// 且输出按 maxStepOutputChars 截断,避免上下文被单步输出撑爆。
|
||
Depends map[string]string
|
||
// Input 是调用方给这次技能执行的初始输入(可为空)。
|
||
Input string
|
||
// Tools 是**已收缩**的本步可用工具面(见 ResolveToolSurface);执行侧不得放宽。
|
||
Tools []string
|
||
// Refs 是技能声明的引用素材。
|
||
Refs []sdk.Ref
|
||
// Spec 是技能的完整新增字段(模型/思考档位/上下文模式/元数据等)。
|
||
Spec Spec
|
||
}
|
||
|
||
// StepResult 是一次步骤执行的结果。
|
||
type StepResult struct {
|
||
// Output 是该步的文本输出(给下游步骤与结果汇总用)。
|
||
Output string
|
||
}
|
||
|
||
// StepExecutor 真正执行技能的某一步(design/09 §6.4-2:每步 = 一次模型调用)。
|
||
//
|
||
// 实现**必须**尊重 ctx:父回合取消后应尽快返回(通常是 ctx.Err())。调度器靠
|
||
// 它在取消/中止时把在飞的步骤收干净,不留下无主的 goroutine。
|
||
type StepExecutor interface {
|
||
ExecuteStep(ctx context.Context, req StepRequest) (StepResult, error)
|
||
}
|
||
|
||
// GuardrailChecker 判断一条 guardrail 是否被违反(design/09 §6.4-4 的强制点)。
|
||
//
|
||
// subject 说明这次校验的对象:逐步校验时是步骤名,收尾全量复核时是
|
||
// GuardrailFinalSubject。**校验本身失败**(无法判定)时返回 err,技能随之中止 ——
|
||
// 绝不把「判不了」当成「通过」。
|
||
type GuardrailChecker interface {
|
||
Check(ctx context.Context, g sdk.Guardrail, subject, output string) (violated bool, err error)
|
||
}
|
||
|
||
// GuardrailFunc 把普通函数适配成 GuardrailChecker。
|
||
type GuardrailFunc func(ctx context.Context, g sdk.Guardrail, subject, output string) (bool, error)
|
||
|
||
// Check 实现 GuardrailChecker;nil 函数一律报错(判不了 ≠ 通过)。
|
||
func (f GuardrailFunc) Check(ctx context.Context, g sdk.Guardrail, subject, output string) (bool, error) {
|
||
if f == nil {
|
||
return false, fmt.Errorf("skills: nil guardrail checker: %w", ErrGuardrailCheck)
|
||
}
|
||
return f(ctx, g, subject, output)
|
||
}
|
||
|
||
// RunOptions 是一次技能执行的入参与依赖(全部由调用方注入,Runner 不自己找)。
|
||
type RunOptions struct {
|
||
// Input 是这次执行的初始输入(用户请求)。
|
||
Input string
|
||
// Executor 是步骤执行器。为 nil ⇒ ErrNoExecutor。
|
||
Executor StepExecutor
|
||
// Gate 是当前安全闸(actor + ToolPolicy + 唤醒态)。为 nil ⇒ ErrNilGate。
|
||
Gate Gate
|
||
// Checker 是 guardrail 校验器。为 nil 时**跳过内容校验**并记一条 WARN:
|
||
// 结构约束(Validate)仍然生效,但「违规」判定无从谈起。
|
||
Checker GuardrailChecker
|
||
// ToolNames 是全局注册表里的工具名全集(通常是本轮 dispatcher 持有的工具)。
|
||
// 为 nil 时从 Runner 的注册表读 KindTool 能力的名字(需要注册表)。
|
||
ToolNames []string
|
||
// MaxParallel 是无依赖步骤的并行上限:非正取默认 2,超过硬上限 3 时夹到 3。
|
||
MaxParallel int
|
||
}
|
||
|
||
// StepOutcome 是一步的执行结果(按**声明顺序**汇报,因此输出确定)。
|
||
type StepOutcome struct {
|
||
// Name 是步骤名。
|
||
Name string
|
||
// Output 是该步的文本输出。
|
||
Output string
|
||
}
|
||
|
||
// Violation 是一条 guardrail 违规事实。enforced 违规会让执行中止,其余只 WARN。
|
||
type Violation struct {
|
||
// GuardrailID 是被违反的 guardrail id。
|
||
GuardrailID string
|
||
// Subject 是校验对象:步骤名,或 GuardrailFinalSubject。
|
||
Subject string
|
||
// Message 是 guardrail 自己声明的规则文本。
|
||
Message string
|
||
// Enforced 报告该条是否强制(为真即已中止技能)。
|
||
Enforced bool
|
||
}
|
||
|
||
// RunResult 是一次技能执行的如实汇报。
|
||
type RunResult struct {
|
||
// Skill 是技能名。
|
||
Skill string
|
||
// Tools 是本次执行收缩后实际可用的工具面(能力收缩的证据)。
|
||
Tools []string
|
||
// Steps 是**已完成**步骤的结果,按声明顺序排列。执行中止时只含已完成的部分
|
||
// (如实汇报,绝不用空结果冒充完成)。
|
||
Steps []StepOutcome
|
||
// Violations 是本次执行发现的所有 guardrail 违规(含 WARN 级)。
|
||
Violations []Violation
|
||
}
|
||
|
||
// Runner 展开并执行技能。它持有全局能力注册表,用于按名找技能、以及在未显式给出
|
||
// 工具全集时读出注册表里的工具面。
|
||
//
|
||
// Runner 本身无可变状态(注册表内部自带锁),因此并发安全。
|
||
type Runner struct {
|
||
reg *registry.Registry
|
||
log *slog.Logger
|
||
}
|
||
|
||
// NewRunner 在 reg 之上构造 Runner;reg 供 ExpandByName / Run 按名查找技能用,
|
||
// 也可以为 nil(此时只能用 RunSkill 直接执行)。log 可以为 nil。
|
||
func NewRunner(reg *registry.Registry, log *slog.Logger) *Runner {
|
||
return &Runner{reg: reg, log: sourcedLog(log)}
|
||
}
|
||
|
||
// Expand 把 s 展开成 Workflow,并复制步骤/引用/约束切片(调用方拥有结果)。
|
||
// s 为 nil 或名字为空时返回 ErrInvalidSkill。
|
||
func (r *Runner) Expand(ctx context.Context, s sdk.Skill) (Workflow, error) {
|
||
if err := ctx.Err(); err != nil {
|
||
return Workflow{}, fmt.Errorf("skills: runner: expand: %w", err)
|
||
}
|
||
if s == nil || strings.TrimSpace(s.Name()) == "" {
|
||
return Workflow{}, fmt.Errorf("skills: runner: expand: %w", ErrInvalidSkill)
|
||
}
|
||
spec := sdk.SkillView(s)
|
||
wf := Workflow{
|
||
Name: s.Name(),
|
||
Description: s.Description(),
|
||
Prompt: s.Prompt(),
|
||
Steps: cloneSteps(s.Steps()),
|
||
Refs: cloneRefs(s.Refs()),
|
||
Guardrails: cloneGuardrails(s.Guardrails()),
|
||
Spec: specOf(spec),
|
||
}
|
||
r.log.Info("skill expanded", "skill", wf.Name, "steps", len(wf.Steps))
|
||
return wf, nil
|
||
}
|
||
|
||
// ExpandByName 按名从注册表找技能并展开。Runner 没有注册表时返回
|
||
// ErrNilRegistrar;名字不存在时返回注册表的 registry.ErrNotFound;注册表里该名字
|
||
// 不是技能时返回 ErrNotFound。
|
||
func (r *Runner) ExpandByName(ctx context.Context, name string) (Workflow, error) {
|
||
s, err := r.lookup(name)
|
||
if err != nil {
|
||
return Workflow{}, err
|
||
}
|
||
return r.Expand(ctx, s)
|
||
}
|
||
|
||
// Run 按名从注册表取技能并**真执行**(design/09 §6.4)。
|
||
//
|
||
// 错误分类:Runner 无注册表 ⇒ ErrNilRegistrar;技能不存在 ⇒ registry.ErrNotFound;
|
||
// 定义不合法 ⇒ Validate 的可分类错误;缺安全闸 ⇒ ErrNilGate;缺执行器 ⇒
|
||
// ErrNoExecutor;某步失败 ⇒ ErrStepFailed(带步骤名);enforced guardrail 违规 ⇒
|
||
// ErrGuardrailViolation(带 guardrail 与对象);父 ctx 取消 ⇒ ctx.Err()。
|
||
//
|
||
// 出错时返回的 RunResult 里只有**已完成**的步骤(诚实的部分结果)。
|
||
func (r *Runner) Run(ctx context.Context, name string, opts RunOptions) (RunResult, error) {
|
||
s, err := r.lookup(name)
|
||
if err != nil {
|
||
return RunResult{Skill: name}, err
|
||
}
|
||
return r.RunSkill(ctx, s, opts)
|
||
}
|
||
|
||
// RunSkill 直接执行一个技能(不经注册表按名查找)。
|
||
//
|
||
// 执行语义(design/09 §6.4):
|
||
//
|
||
// - 步骤按 depends 拓扑调度,无依赖的步骤可并行(默认 2 路,硬上限 3);
|
||
// - 每步的可用工具面 = allowed-tools ∩ 安全闸 ∩ 注册表 − disallowed-tools;
|
||
// - 每步输出后校验 guardrail:enforced 违反即中止并如实报「哪条、哪一步」;
|
||
// 非 enforced 只 WARN 后继续;全部步骤完成后再做一次累计输出的全量复核;
|
||
// - 任一步失败即整体失败(不静默跳过);父 ctx 取消即整体取消。
|
||
func (r *Runner) RunSkill(ctx context.Context, s sdk.Skill, opts RunOptions) (RunResult, error) {
|
||
if s == nil || strings.TrimSpace(s.Name()) == "" {
|
||
return RunResult{}, fmt.Errorf("skills: runner: run: %w", ErrInvalidSkill)
|
||
}
|
||
name := s.Name()
|
||
result := RunResult{Skill: name}
|
||
if opts.Executor == nil {
|
||
return result, fmt.Errorf("skills: runner: run %q: %w", name, ErrNoExecutor)
|
||
}
|
||
if opts.Gate == nil {
|
||
// 缺安全闸 ⇒ 一律拒。**不能**因为没有闸就把工具全放出去(只收不放)。
|
||
return result, fmt.Errorf("skills: runner: run %q: %w", name, ErrNilGate)
|
||
}
|
||
if err := Validate(s); err != nil {
|
||
return result, fmt.Errorf("skills: runner: run %q: %w", name, err)
|
||
}
|
||
tools, err := r.toolSurface(ctx, s, opts)
|
||
if err != nil {
|
||
return result, err
|
||
}
|
||
result.Tools = tools
|
||
if opts.Checker == nil && len(s.Guardrails()) > 0 {
|
||
r.log.Warn("skill guardrails have no checker; content checks are skipped",
|
||
"skill", name, "guardrails", len(s.Guardrails()))
|
||
}
|
||
env := &runEnv{
|
||
skill: s,
|
||
spec: specOf(sdk.SkillView(s)),
|
||
tools: tools,
|
||
input: opts.Input,
|
||
exec: opts.Executor,
|
||
checker: opts.Checker,
|
||
parallel: clampParallel(opts.MaxParallel),
|
||
log: r.log,
|
||
}
|
||
return env.run(ctx, result)
|
||
}
|
||
|
||
// lookup 按名从注册表取技能。
|
||
func (r *Runner) lookup(name string) (sdk.Skill, error) {
|
||
if r.reg == nil {
|
||
return nil, fmt.Errorf("skills: runner: lookup %q: %w", name, ErrNilRegistrar)
|
||
}
|
||
cap, err := r.reg.Get(name)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("skills: runner: lookup %q: %w", name, err)
|
||
}
|
||
skill, ok := cap.Value.(sdk.Skill)
|
||
if !ok {
|
||
return nil, fmt.Errorf("skills: runner: lookup %q: %w", name, ErrNotFound)
|
||
}
|
||
return skill, nil
|
||
}
|
||
|
||
// toolSurface 求本次执行的工具面:allowed-tools ∩ 安全闸 ∩ 注册表 − disallowed-tools。
|
||
func (r *Runner) toolSurface(ctx context.Context, s sdk.Skill, opts RunOptions) ([]string, error) {
|
||
if err := ctx.Err(); err != nil {
|
||
return nil, fmt.Errorf("skills: runner: run %q: %w", s.Name(), err)
|
||
}
|
||
spec := sdk.SkillView(s)
|
||
registered := opts.ToolNames
|
||
if registered == nil {
|
||
names, err := r.registeredToolNames()
|
||
if err != nil {
|
||
return nil, fmt.Errorf("skills: runner: run %q: %w", s.Name(), err)
|
||
}
|
||
registered = names
|
||
}
|
||
return ResolveToolSurface(registered, spec.AllowedTools(), spec.DisallowedTools(), opts.Gate), nil
|
||
}
|
||
|
||
// registeredToolNames 读出注册表里 KindTool 能力的名字(排序后返回,因此本次执行的
|
||
// 工具面与日志都是确定的)。注册表缺位时报 ErrNilRegistrar —— 工具面必须来自真实的
|
||
// 注册表,绝不凭空造一个空/全量集合。
|
||
func (r *Runner) registeredToolNames() ([]string, error) {
|
||
if r.reg == nil {
|
||
return nil, ErrNilRegistrar
|
||
}
|
||
caps := r.reg.List(registry.KindTool)
|
||
names := make([]string, 0, len(caps))
|
||
for _, c := range caps {
|
||
names = append(names, c.Name)
|
||
}
|
||
sort.Strings(names)
|
||
return names, nil
|
||
}
|
||
|
||
// specOf 把 sdk.SkillSpec 读成 Spec(同时补上归一化缺省值),使执行侧只认一种形状。
|
||
func specOf(spec sdk.SkillSpec) Spec {
|
||
if spec == nil {
|
||
return normalizeSpec(Spec{})
|
||
}
|
||
return normalizeSpec(Spec{
|
||
Version: spec.Version(),
|
||
WhenToUse: spec.WhenToUse(),
|
||
Invocation: spec.Invocation(),
|
||
Triggers: spec.Triggers(),
|
||
Paths: spec.Paths(),
|
||
AllowedTools: spec.AllowedTools(),
|
||
DisallowedTools: spec.DisallowedTools(),
|
||
Context: spec.ContextMode(),
|
||
Agent: spec.Agent(),
|
||
Model: spec.Model(),
|
||
Effort: spec.Effort(),
|
||
Metadata: spec.Metadata(),
|
||
})
|
||
}
|