// signals.go holds the intent layer vocabulary and the L1/L2/L3 layers; the // package doc lives in capacity.go. package capacity import ( "context" "strings" ) // 内置档位集里「后加的能力」的工具名(字面量,与既有条目同一口径: // 这两份集合是**策略清单**,不反向依赖具体能力包)。两处都登记是 X1 的 // 硬要求 —— 能力注册了却不在允许集里,等于 CAPACITY_ENABLED=true 时它调不动: // // - "transcode"(internal/capability/transcode,design/25):纯本地、有输入上限 // 与并发闸,属陪伴侧真正会用的能力(语音转写); // - "capability_list"(internal/capability/registry 的 ToolName,X1 / 裁决 15): // 她自查「我有哪些能力/插件、外围子系统什么状态」的**只读**抓手,必须在她 // 思考时所处的静息态就可见,否则这条裁决在安静态失效。 // // 重命名任一个 ⇒ internal/runtime/capacity/capacity_test.go 的 // TestDecide_AllowedSets 与 cmd/cyrene 的组合根断言同时变红(见 X1 报告变异表)。 const ( transcodeToolName = "transcode" capabilityListToolName = "capability_list" // selfWakeToolName 是「自排一次思考」工具的名字(internal/services/backthink 的 // ThinkToolName,design/12 §N C.3)。它必须**同时**进下面两张内置允许集: // 她思考时通常落在静息态(陪伴 / 夜里),只进唤醒态等于「注册了却看不见」。 // // ⚠️ 主人若用既有的 `CAPACITY_TOOL_SETS` 覆盖档位集,这两张内置表会被**整体替换** // (`wire_capacity.go` 的 parseCapacityToolSets),那份 JSON 里没写 request_think // 就照样看不见 —— 该键是「替换」不是「追加」,这条口径与 X1 的同类缺口同源。 selfWakeToolName = "request_think" ) // defaultQuietTools is the companion-safe face (design §1): terminal + file edit // + memory/emotion companion. It is a function building the slice fresh so no // package-level global slice is introduced (project rule §10). func defaultQuietTools() []string { return []string{"terminal", "file", "knowledge", "text", transcodeToolName, capabilityListToolName, selfWakeToolName} } // defaultAwakeTools extends the quiet face with web/network retrieval (design // §1 唤醒态): a real task wakes the retrieval tools, file and terminal. func defaultAwakeTools() []string { return []string{ "terminal", "file", "knowledge", "text", "web-search", "web-fetch", "http", transcodeToolName, capabilityListToolName, selfWakeToolName, } } // intent classifies the round. It runs L1 on the current turn, falls to L2 on // the recent context trend, then to the model-assisted L3 layer when it is // enabled, and folds in the L4 learned-habit verdict (design §2.2) at each layer: // L4 may correct a decided result (boost / quiet→awake) or fill in when L1/L2/L3 // are undecided. With no matching habit L4 is silent, so a turn with no recall // behaves exactly as before; otherwise the result stays conservative (低置信 still // rests via applyAwakening). func (d *Decider) intent(ctx context.Context, in Input) (State, float64, string) { if len(in.Messages) == 0 { return "", 0, "" } current := in.Messages[len(in.Messages)-1] l4s, l4c, l4t, l4ok := d.l4(current, in.Recall) if s, c, t, ok := l1(current); ok { return d.mergeL4(s, c, t, l4s, l4c, l4t, l4ok) } if s, c, t, ok := l2(in.Messages); ok { return d.mergeL4(s, c, t, l4s, l4c, l4t, l4ok) } if s, c, t, ok := d.l3(ctx, current); ok { return d.mergeL4(s, c, t, l4s, l4c, l4t, l4ok) } if l4ok { return l4s, l4c, l4t } return "", 0.3, "" // 拿不准 → conservative fallback } // l3 runs the model-assisted intent layer (design/14 §2.2 L3; 裁决 14.4.4): the // configured classifier judges the current turn and its verdict is trusted only // at or above the wake threshold — a low-confidence verdict is treated as // undecided, so a hesitant model can never wake tools on its own. A classifier // error is reported and treated the same way. With the layer off this returns // ok=false immediately, leaving L1/L2/L4 behavior byte-identical. func (d *Decider) l3(ctx context.Context, current string) (State, float64, string, bool) { if d.l3cache == nil { return "", 0, "", false } j, err := d.l3cache.classify(ctx, current) if err != nil { d.log.Warn("capacity L3 classifier unavailable, keeping the conservative default", "error", err) return "", 0, "", false } if j.Confidence < d.cfg.MinConfidence { d.log.Debug("capacity L3 verdict below threshold, treated as undecided", "confidence", j.Confidence, "threshold", d.cfg.MinConfidence, "is_task", j.IsTask) return "", 0, "", false } if !j.IsTask { return StateQuiet, j.Confidence, "", true } if j.TaskKind == taskKindComplex { return StateFull, j.Confidence, inferTool(current), true } return StateAwake, j.Confidence, inferTool(current), true } // l1 is the fast heuristic on the current turn (design §2.2 L1): a strong // action signal wakes (awake, or full for complex work); a clear companion // signal (weak + no task object) stays quiet; otherwise it is undecided and the // caller defers to L2. func l1(current string) (State, float64, string, bool) { if hasStrongSignal(current) { if isComplex(current) { return StateFull, 0.9, inferTool(current), true } return StateAwake, 0.9, inferTool(current), true } if isClearCompanion(current) { return StateQuiet, 0.8, "", true } return "", 0, "", false } // l2 looks at the recent context trend (design §2.2 L2) when L1 is undecided: // a session firmly leaning task (or a follow-up on the previous task) wakes; // a session firmly leaning emotional keeps quiet; a balanced window defers. func l2(messages []string) (State, float64, string, bool) { window := lastN(messages, 6) task, comp := 0, 0 lastTask := "" for _, m := range window { switch { case hasStrongSignal(m): task++ lastTask = m case isClearCompanion(m): comp++ case hasFollowup(m): task++ } } if task > comp { if isComplex(lastTask) { return StateFull, 0.66, inferTool(lastTask), true } return StateAwake, 0.66, inferTool(lastTask), true } if comp > task { return StateQuiet, 0.7, "", true } return "", 0, "", false } // lastN returns the last n elements of messages (or all when shorter). func lastN(messages []string, n int) []string { if len(messages) <= n { return messages } return messages[len(messages)-n:] } // hasStrongSignal reports a task-verb present together with either an // imperative marker (帮我/请/去/把/将) or a concrete task object (file path, // URL, code, a task noun) — 明确的动作指令 + 任务对象 (design §2.2 L1). func hasStrongSignal(text string) bool { if !hasStrongVerb(text) { return false } return hasImperativeMarker(text) || hasTaskObject(text) } // hasStrongVerb reports an action verb that implies work (查/改/写/搜/跑/部署…). func hasStrongVerb(text string) bool { return containsAny(text, strongVerbs()) } // strongVerbs is the L1 action-verb vocabulary. func strongVerbs() []string { return []string{ "帮我", "查", "查询", "查一下", "搜索", "搜", "搜一下", "查找", "找一下", "找", "看", "看看", "检查", "核对", "改", "修改", "编辑", "写", "编写", "创建", "生成", "实现", "配置", "修复", "删除", "移除", "升级", "更新", "搭建", "部署", "迁移", "重构", "运行", "执行", "跑", "编译", "构建", "安装", "启动", "重启", "停止", "下载", "上传", "翻译", "计算", "对比", "分析", "总结", "汇报", "审批", "编排", // ASCII "help me", "search", "query", "look up", "find", "edit", "modify", "write", "create", "generate", "fix", "refactor", "migrate", "deploy", "build", "run", "compile", "install", "start", "restart", "stop", "test", "analyze", "calculate", "compare", "please", } } // hasImperativeMarker reports a direct instruction cue (帮我/请/去/把/将/麻烦…). func hasImperativeMarker(text string) bool { return containsAny(text, imperativeMarkers()) } func imperativeMarkers() []string { return []string{ "帮我", "请", "给我", "麻烦你", "你能", "你可不可以", "去", "把", "将", "帮忙", "拜托", "帮我一下", // ASCII "please", "help me", "can you", "could you", "would you", } } // hasTaskObject reports a concrete task object: a file path, URL, code snippet, // or an object noun (报告/文档/代码/测试/设备…). func hasTaskObject(text string) bool { return hasFileObject(text) || hasURLObject(text) || hasCodeObject(text) || hasTaskNoun(text) } // hasFileObject reports a path-like target (a known source extension, a drive // path, a "./" or "../" relative path, a bare filename such as cx.go). func hasFileObject(text string) bool { for _, ext := range fileExtensions() { if strings.Contains(text, ext) { return true } } if strings.Contains(text, "./") || strings.Contains(text, "../") { return true } return false } // fileExtensions is the source/config extension set used for object detection. func fileExtensions() []string { return []string{ ".go", ".ts", ".tsx", ".js", ".jsx", ".py", ".rs", ".java", ".c", ".cpp", ".h", ".yaml", ".yml", ".json", ".toml", ".ini", ".conf", ".mod", ".sum", ".md", ".txt", ".css", ".html", ".sh", ".sql", ".proto", ".env.example", } } // hasURLObject reports an http(s) URL or a bare web host. func hasURLObject(text string) bool { return containsAny(text, []string{"http://", "https://", "www.", ".com", ".cn", ".net", ".org", ".io", ".dev", ".xyz", ".ai"}) } // hasCodeObject reports a code snippet (backticks, brace pairs, declarations). func hasCodeObject(text string) bool { return containsAny(text, []string{"`", "func ", "def ", "return ", ":= ", "class ", "import ", "package ", "const ", "var ", "=>", "};"}) } // hasTaskNoun reports a concrete work object noun. func hasTaskNoun(text string) bool { return containsAny(text, taskNouns()) } // taskNouns is the object-noun vocabulary for object detection. func taskNouns() []string { return []string{ "报告", "文档", "代码", "脚本", "测试", "文件", "项目", "结果", "数据", "接口", "配置", "日志", "表格", "图片", "网页", "页面", "服务器", "服务", "设备", "手机", "电脑", "城市", "天气", "新闻", "价格", "股票", "汇率", "系统", "数据库", "模型", "论文", "资料", "链接", // ASCII "report", "doc", "code", "script", "test", "file", "project", "server", "device", "config", "log", "url", "api", "database", "page", } } // isClearCompanion reports a pure companionship turn: a weak (emotional/greeting) // signal with no task verb and no task object (设计 §2.2 L1 弱信号→静息). func isClearCompanion(text string) bool { if !hasWeakSignal(text) { return false } if hasStrongSignal(text) { return false } return !hasTaskObject(text) } // hasWeakSignal reports a companion-leaning cue (问候/情绪词/日常寒暄). func hasWeakSignal(text string) bool { return containsAny(text, weakSignals()) } // weakSignals is the L1 companion vocabulary. func weakSignals() []string { return []string{ "你好", "嗨", "哈喽", "早上好", "早安", "中午好", "下午好", "晚上好", "晚安", "在吗", "在不在", "抱抱", "么么", "亲亲", "摸摸", "想你", "爱你", "喜欢你", "想念", "我好", "我累", "累了", "疲惫", "困了", "无聊", "寂寞", "失眠", "睡不着", "难过", "伤心", "开心", "高兴", "哈哈", "嘿嘿", "呜呜", "555", "哭", "笑", "心疼", "安慰", "谢谢", "辛苦", "再见", "明天见", "待会见", "怎么了", "在干嘛", "干嘛呢", "好呀", "好的", "好哒", "嗯嗯", "嗯", "好", "喔", "哦", "呀", // ASCII "hi", "hello", "hey", "good morning", "good night", "good evening", "hug", "miss you", "love you", "haha", "lol", "aww", "ty", "thanks", "bye", "thinking", "sleepy", "tired", "happy", "mood", "nice", } } // hasFollowup reports a continuation cue pointing at the previous task // (设计 §2.2 L2: 上一轮任务的跟进 继续/然后呢/还有吗). func hasFollowup(text string) bool { return containsAny(text, followups()) } func followups() []string { return []string{ "继续", "然后呢", "还有吗", "还有呢", "接着", "下一步", "接下来", "之后呢", "那然后", "继续做", "继续写", "继续说", "再来", "再继续", // ASCII "continue", "and then", "next", "what else", "also", "go on", "more", } } // isComplex reports whether a task is complex enough for the full state // (设计 §1 全能态·复杂任务): refactor/migrate/deploy/whole-project/multi-file… func isComplex(text string) bool { return containsAny(text, complexMarkers()) } func complexMarkers() []string { return []string{ "重构", "搭建", "部署", "迁移", "架构", "实现一个", "写一个", "做一个", "整个项目", "全部", "所有", "批量", "多文件", "多个文件", "全量", "综合", "一体化", "测试全部", "修复所有", "上下链路", "系统设计", // ASCII "refactor", "migrate", "architect", "build a", "full", "all the", "multiple files", "entire", "whole", "batch", } } // inferTool names the tool that a task turn would light up, used for the // capacity_awaken event's Tool field (点亮工具). Order: file path/code → file, // query/search → web-search, else terminal. func inferTool(text string) string { switch { case hasFileObject(text), hasCodeObject(text): return "file" case containsAny(text, []string{ "查", "查询", "搜", "搜索", "查找", "找一下", "search", "query", "look up", "find", "查一下", "搜索一下", }): return "web-search" case containsAny(text, []string{ "跑", "运行", "执行", "部署", "编译", "构建", "安装", "启动", "重启", "测试", "run", "deploy", "build", "compile", "install", "start", "restart", "exec", "test", }): return "terminal" default: return "" } } // containsAny reports whether text contains any of words (a simple substring // match; every word is a UTF-8 substring). func containsAny(text string, words []string) bool { for _, w := range words { if strings.Contains(text, w) { return true } } return false }