feat(skills): 技能加载/校验/真执行第一片(design/09 §6 定稿落地)
- 加载器/校验:目录式 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 的分阶段)。
This commit is contained in:
1 parent
29181245ce
commit
f75e7a9c9b
17 files changed
+3067
-164
No files matched your search
@@ -0,0 +1,124 @@
|
||||
package skills
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
|
||||
)
|
||||
|
||||
// 本文件实现 design/09 §6.3 的**清单投影**:模型在清单里只看到 name +
|
||||
// description(+when-to-use),**正文(prompt / 步骤渲染文本)不进清单**。正文只在
|
||||
// 技能命中后才展开注入(注入点走 design/14 的唤醒式口径,落在 loop 片)。
|
||||
//
|
||||
// 这里只做「投影 + 预算 + 三态」这一层纯计算,不读盘、不写上下文。
|
||||
|
||||
const (
|
||||
// SkillListingDescriptionBudget 是单条 description(+when-to-use) 的字符上限
|
||||
// (design/09 §6.3-2:照 Agent Skills listing 的 1536 字符上限)。
|
||||
SkillListingDescriptionBudget = 1536
|
||||
// SkillListingMaxEntries 是清单条数的全局上限。超出时**如实标 truncated**,
|
||||
// 绝不静默丢条目(design/09 §6.3-2)。
|
||||
SkillListingMaxEntries = 64
|
||||
// SkillListingTruncatedMarker 是超预算时追加的显式标记。
|
||||
SkillListingTruncatedMarker = "…(truncated)"
|
||||
)
|
||||
|
||||
// ListingEntry 是清单里的一行。它只带名字与描述 —— 正文不在其中。
|
||||
type ListingEntry struct {
|
||||
// Name 是技能名。
|
||||
Name string
|
||||
// Description 是 description 与 when-to-use 拼成的一句说明(按预算截断);
|
||||
// invocation=model-only 时为空(该调用态下清单不带描述,省上下文)。
|
||||
Description string
|
||||
// ModelInvocable 报告模型是否可以调用该技能(invocation=user-only 为 false)。
|
||||
ModelInvocable bool
|
||||
// UserInvocable 报告人是否可以显式调用该技能(invocation=model-only 为 false)。
|
||||
UserInvocable bool
|
||||
// Truncated 报告本行描述是否被预算截断。
|
||||
Truncated bool
|
||||
}
|
||||
|
||||
// Listing 是清单投影的结果。
|
||||
type Listing struct {
|
||||
// Entries 是清单条目(保持输入顺序)。
|
||||
Entries []ListingEntry
|
||||
// Truncated 报告条目数是否因 SkillListingMaxEntries 被截断。
|
||||
Truncated bool
|
||||
// Note 是截断的人类可读说明(未截断时为空)。
|
||||
Note string
|
||||
}
|
||||
|
||||
// BuildListing 把技能列表投影成模型可见的清单:三态过滤 + 单条预算 + 条数上限。
|
||||
//
|
||||
// 三态(design/09 §6.3-4,与 Agent Skills 的矩阵逐格对齐):
|
||||
//
|
||||
// - both ⇒ 清单含 description,人与模型都可调;
|
||||
// - model-only ⇒ 清单**不含** description,只有模型能调;
|
||||
// - user-only ⇒ 清单含 description,**模型不可调**。
|
||||
//
|
||||
// nil 技能被跳过。**所有**技能都会出现在清单里(user-only 的技能只是模型不可调,
|
||||
// 但仍要让人在面板里看得到),因此条数上限对三类一视同仁。
|
||||
func BuildListing(list []sdk.Skill) Listing {
|
||||
out := Listing{}
|
||||
limit := SkillListingMaxEntries
|
||||
for _, s := range list {
|
||||
if s == nil {
|
||||
continue
|
||||
}
|
||||
if len(out.Entries) >= limit {
|
||||
out.Truncated = true
|
||||
break
|
||||
}
|
||||
out.Entries = append(out.Entries, listingEntry(s))
|
||||
}
|
||||
if out.Truncated {
|
||||
out.Note = "skill listing truncated to " + strconv.Itoa(limit) + " entries"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// listingEntry 投影一个技能。
|
||||
func listingEntry(s sdk.Skill) ListingEntry {
|
||||
invocation := sdk.SkillView(s).Invocation().OrDefault()
|
||||
entry := ListingEntry{
|
||||
Name: s.Name(),
|
||||
ModelInvocable: invocation.ModelInvocable(),
|
||||
UserInvocable: invocation.UserInvocable(),
|
||||
}
|
||||
if !invocation.ListingHasDescription() {
|
||||
return entry
|
||||
}
|
||||
text := strings.TrimSpace(joinDescription(s.Description(), sdk.SkillView(s).WhenToUse()))
|
||||
entry.Description, entry.Truncated = truncateBudget(text, SkillListingDescriptionBudget)
|
||||
return entry
|
||||
}
|
||||
|
||||
// joinDescription 把 description 与 when-to-use 合成清单里那一句(两者都空则为空)。
|
||||
func joinDescription(description, whenToUse string) string {
|
||||
parts := make([]string, 0, 2)
|
||||
if trimmed := strings.TrimSpace(description); trimmed != "" {
|
||||
parts = append(parts, trimmed)
|
||||
}
|
||||
if trimmed := strings.TrimSpace(whenToUse); trimmed != "" {
|
||||
parts = append(parts, "Use when: "+trimmed)
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// truncateBudget 按 rune 截断到 n 个字符以内,并报告是否真的截断了(截断标记也
|
||||
// 计入预算,保证结果长度不超过 n)。
|
||||
func truncateBudget(s string, n int) (string, bool) {
|
||||
if n <= 0 {
|
||||
return s, false
|
||||
}
|
||||
runes := []rune(s)
|
||||
if len(runes) <= n {
|
||||
return s, false
|
||||
}
|
||||
marker := []rune(SkillListingTruncatedMarker)
|
||||
if n <= len(marker) {
|
||||
return string(runes[:n]), true
|
||||
}
|
||||
return string(runes[:n-len(marker)]) + SkillListingTruncatedMarker, true
|
||||
}
|
||||
Reference in new issue
Block a user