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(), }) }