Files
Cyrene_v2/internal/runtime/loop/assemble_skill_workfiles.go
AskaEth 6475eb875d 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 启发式(只认显式挂载);多技能并集;收缩作用域=激活的那个回合。
2026-10-06 14:12:24 +08:00

93 lines
3.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}