- 加载器/校验:目录式 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 的分阶段)。
125 lines
4.4 KiB
Go
125 lines
4.4 KiB
Go
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
|
||
}
|