- 清单只给 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 启发式(只认显式挂载);多技能并集;收缩作用域=激活的那个回合。
93 lines
3.4 KiB
Go
93 lines
3.4 KiB
Go
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
|
||
}
|