feat(skills): 渐进披露接线(L2)—— 清单投影 + 命中才展开 + 工具面只收不放
- 清单只给 name+description(+when-to-use),单条 ≤1536 字符 + 全局条数上限,超限如实标 truncated - 命中才把正文作为一条消息注入:跨回合保留、按内容指纹缓存不重复读盘、注入 ≤16 KiB 标 .truncated - invocation 三态与清单逐格对齐(model-only 清单不含 description;user-only 清单有但模型不可调) - 激活轮的工具面 = allowed-tools ∩ 安全闸 ∩ 注册表 再减 disallowed-tools(owner 也看不见未允许的工具) - 激活状态进程内按会话(不落库);注入量已界化(压缩器估值未含,超我文件面) 裁定(DECISIONS #3/#4/#5):删掉 paths 启发式(只认显式挂载);多技能并集;收缩作用域=激活的那个回合。
This commit is contained in:
1 parent
2e1c3ed3fd
commit
6475eb875d
10 files changed
+1448
-3
No files matched your search
@@ -0,0 +1,92 @@
|
||||
package loop
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
)
|
||||
|
||||
// 本文件汇出技能 `paths` 判定(DevDocs/design/09-插件生态.md §6.2/§6.3)要用的
|
||||
// 「本轮工作文件」:ctx 上显式挂载的权威路径,加上用户本轮文本里看得出的路径
|
||||
// token(保守启发式,代价只落在上下文,不落在权限上)。
|
||||
|
||||
// skillWorkFilesKey 是「本轮工作文件」的 context key(design/09 §6.2 的 `paths`)。
|
||||
type skillWorkFilesKey struct{}
|
||||
|
||||
// WithSkillWorkFiles 把本轮确实在动的文件路径挂到 ctx 上,供技能 `paths` 判定
|
||||
// 自动激活(design/09 §6.3)。知道真实路径的调用方(网关的附件/工作区、编码类
|
||||
// 工具的宿主)挂它;未挂载时该来源为空,判定退回用户文本里的路径候选。
|
||||
//
|
||||
// 空列表是空操作:不挂键即「本轮没有工作文件」,与不接该能力时逐字一致。
|
||||
func WithSkillWorkFiles(ctx context.Context, paths []string) context.Context {
|
||||
if ctx == nil || len(paths) == 0 {
|
||||
return ctx
|
||||
}
|
||||
return context.WithValue(ctx, skillWorkFilesKey{}, append([]string(nil), paths...))
|
||||
}
|
||||
|
||||
// SkillWorkFilesFrom 返回 ctx 上挂载的工作文件;未挂载返回 nil。它是
|
||||
// WithSkillWorkFiles 的读侧,供只拿到 context 的调用方与测试观察。
|
||||
func SkillWorkFilesFrom(ctx context.Context) []string {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
paths, _ := ctx.Value(skillWorkFilesKey{}).([]string)
|
||||
return paths
|
||||
}
|
||||
|
||||
// turnWorkFiles 汇出本轮 `paths` 判定的工作文件候选:
|
||||
//
|
||||
// - ctx 上显式挂载的(WithSkillWorkFiles,权威来源);
|
||||
// - 用户本轮文本里**看得出是路径**的 token(保守启发式,见 pathLikeTokens)。
|
||||
//
|
||||
// 第二条是为了让「帮我改 internal/runtime/loop/run.go」这类消息真的能命中
|
||||
// `paths`,而不是让这枚设计字段在生产上永远不生效。它只影响「是否自动展开某个
|
||||
// 技能的正文」,不参与任何身份/权限判定(身份只走 channel 层 actor 元数据)。
|
||||
func turnWorkFiles(ctx context.Context, messages []model.Message) []string {
|
||||
hinted := SkillWorkFilesFrom(ctx)
|
||||
derived := pathLikeTokens(lastUserText(messages))
|
||||
if len(hinted) == 0 {
|
||||
return derived
|
||||
}
|
||||
return append(append(make([]string, 0, len(hinted)+len(derived)), hinted...), derived...)
|
||||
}
|
||||
|
||||
// pathLikeTokens 从一段文本里挑出「像路径」的 token:含路径分隔符(`/` 或 `\`)、
|
||||
// 不含空白/引号等边界字符、单 token 长度有界。刻意保守 —— 宁可漏报,也不把普通
|
||||
// 词当成路径(误报只会多展开一份技能正文,代价是上下文,不是权限)。
|
||||
func pathLikeTokens(text string) []string {
|
||||
const (
|
||||
maxTokenLen = 200
|
||||
maxTokens = 16
|
||||
)
|
||||
if text == "" {
|
||||
return nil
|
||||
}
|
||||
fields := strings.FieldsFunc(text, func(r rune) bool {
|
||||
switch r {
|
||||
case ' ', '\t', '\n', '\r', '"', '\'', '`', ',', ';', '(', ')', '[', ']', '{', '}',
|
||||
'<', '>', '|', ',', '。', '、', '(', ')', ':', ';', '「', '」':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
out := make([]string, 0, 4)
|
||||
seen := make(map[string]struct{}, 4)
|
||||
for _, tok := range fields {
|
||||
if len(tok) > maxTokenLen || !strings.ContainsAny(tok, `/\`) {
|
||||
continue
|
||||
}
|
||||
if _, dup := seen[tok]; dup {
|
||||
continue
|
||||
}
|
||||
seen[tok] = struct{}{}
|
||||
out = append(out, tok)
|
||||
if len(out) >= maxTokens {
|
||||
break
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user