package capacity import ( "context" "math" "strconv" "strings" ) // RecallItem is one recalled memory hit handed to the decider for L4 habit // alignment (design §2.2 L4). It is a decoupling view over memory.Result so the // capacity package stays independent of the memory subsystem; the loop builds it // from Retriever.Retrieve. Scope carries the memory scope value ("user" / "persona" // / "project") so L4 can honor the isolation red line (学行为习惯, 不读陌生人敏感). type RecallItem struct { Content string Scope string Kind string Tags []string Score float64 } // Judgment is the intent outcome of one round, meant to be written back into // memory so the L4 layer learns the user's task-expression habits over time // (design §2.4 闭环学习). It records the raw intent-layer classification (before // any persona override) because the closed loop learns the user's intent pattern, // not the persona's presentation. Text is the user expression that was judged — the // habit key L4 re-matches on later rounds. type Judgment struct { IsTask bool TaskKind string State State Confidence float64 Text string } // L4 tuning constants. const ( // minHabitOverlap is the minimum longest-common-substring length (in runes) a // recalled habit must share with the current turn before L4 treats it as a // matching habit. 3 runes covers a short Chinese phrase (搞一下) while staying // conservative against token noise. minHabitOverlap = 3 // taskHabitConfidence is the wake confidence L4 assigns when it matches a // habit-style memory (not a canonical intent record) that leans 办事. It sits // above the default wake threshold so the habit can actually wake a missed task. taskHabitConfidence = 0.7 // intent kind tokens stored in an intent record and understood by awakenVerdict. taskKindComplex = "complex" taskKindCode = "code" taskKindQuery = "query" taskKindOps = "ops" // intentRecordPrefix marks a canonical intent record written back by RecordIntent. intentRecordPrefix = "intent:" ) // Judge 只跑意图分层(L1 → L2 → L3 → L4),返回该轮的原始判定 // (design §2.4 闭环学习的写回对象):它不施加人格覆盖、不产出事件、 // 不改动任何状态,也不写任何东西。 // // 它是 Decide 的「只看判定」视图:回合落定时调它一次即可拿到该轮判定 // 用于回写记忆(RecordIntent),既不必重跑整次决策,也不会重复触发 // capacity_awaken 广播。判定层被禁用或没有可判文本时返回零值 // (IsTask=false),调用方据此跳过写入。 func (d *Decider) Judge(ctx context.Context, in Input) Judgment { if !d.cfg.Enabled || len(in.Messages) == 0 { return Judgment{} } state, conf, tool := d.intent(ctx, in) return judgmentFor(in, state, conf, tool) } // IsIntentRecord 报告 content 是否为 RecordIntent 写回的规范意图记录 // (design §2.4)。群聊/陌生人的记忆注入据此把主人原话的机器可读形态 // 挡在只读共享块之外:那是 L4 的学习素材与「主人敏感信息」, // 不是给群聊消费的非敏感共享记忆。 func IsIntentRecord(content string) bool { _, ok := parseIntentRecord(content) return ok } // RecordIntent formats a judgment into the canonical intent-record string written // back to memory (design §2.4). Only task judgments are worth learning — a quiet / // non-task judgment returns "" so callers can skip persisting noise. The format is // machine-parseable by parseIntentRecord, which L4 re-reads on later rounds to // close the learning loop. The user expression is written in plain text so the // keyword retriever can still surface it against a later similar turn. func (d *Decider) RecordIntent(j Judgment) string { if !j.IsTask { return "" } return formatIntentRecord(j) } // formatIntentRecord encodes j as one parseable line. The text field comes last and // is taken verbatim (it may contain ';' or '='), so parseIntentRecord reads the // fixed prefix and keeps the remainder as the expression. func formatIntentRecord(j Judgment) string { return intentRecordPrefix + "isTask=" + strconv.FormatBool(j.IsTask) + ";taskKind=" + j.TaskKind + ";confidence=" + strconv.FormatFloat(j.Confidence, 'f', -1, 64) + ";text=" + j.Text } // intentRecord is the parsed form of a canonical intent record. type intentRecord struct { isTask bool taskKind string confidence float64 text string } // parseIntentRecord decodes a canonical intent record written by formatIntentRecord. // ok is false when content is not a well-formed intent record (missing isTask). func parseIntentRecord(content string) (intentRecord, bool) { rest, ok := strings.CutPrefix(content, intentRecordPrefix) if !ok { return intentRecord{}, false } // text is last and taken verbatim, so a ';' or '=' inside it cannot split it. head, text := rest, "" if idx := strings.Index(rest, ";text="); idx >= 0 { head = rest[:idx] text = rest[idx+len(";text="):] } rec := intentRecord{text: text} seenTask := false for _, f := range strings.Split(head, ";") { k, v, cut := strings.Cut(f, "=") if !cut { continue } switch k { case "isTask": rec.isTask = v == "true" seenTask = true case "taskKind": rec.taskKind = v case "confidence": if x, err := strconv.ParseFloat(v, 64); err == nil { rec.confidence = x } } } if !seenTask { return intentRecord{}, false } return rec, true } // l4 is the L4 habit-alignment layer (design §2.2, §2.4): it uses recalled user // habits to correct or fill in the intent that L1/L2 could not settle. It returns // ok=false when no habit applies, so the caller's L1/L2 verdict (or the // conservative fallback) stands — L4 is silent unless a learned habit matches // (向后兼容, 数据缺失时回退). func (d *Decider) l4(current string, recall []RecallItem) (State, float64, string, bool) { text := strings.TrimSpace(current) if text == "" || len(recall) == 0 { return "", 0, "", false } bestState, bestConf, bestTool := State(""), 0.0, "" matched := false for _, it := range recall { if !allowedHabitScope(it.Scope) { continue } if rec, ok := parseIntentRecord(it.Content); ok { // A canonical record written back by RecordIntent: it knows the exact // task kind and confidence. Only a task record with a matching expression // testifies to task; the stored text is plaintext so overlap is matched // against it (tolerating paraphrase). if !rec.isTask || rec.text == "" || overlapLen(text, rec.text) < minHabitOverlap { continue } state, tool := awakenVerdict(rec.taskKind) conf := clampConf(rec.confidence) if conf == 0 { conf = taskHabitConfidence } if conf > bestConf { bestState, bestConf, bestTool, matched = state, conf, tool, true } continue } // Habit-style memory (not written by us): treat it as a task habit only when // it leans 办事 and shares a phrase with the current turn, so an unrelated // preference (coffee habit) never triggers a wake. if !leansTask(it.Content) || overlapLen(text, it.Content) < minHabitOverlap { continue } conf := taskHabitConfidence if it.Score > 0 { conf = clampConf(taskHabitConfidence + it.Score*0.15) } state, tool := StateAwake, inferTool(text) if isComplex(text) { state = StateFull } if conf > bestConf { bestState, bestConf, bestTool, matched = state, conf, tool, true } } if !matched { return "", 0, "", false } return bestState, bestConf, bestTool, true } // mergeL4 combines an L1/L2 verdict with the L4 habit verdict (design §2.2 合并规则). // It is conservative: a strong learned habit may count a task the heuristics missed // (quiet → awake, a 举手示意 but never a jump straight to full) or confirm/boost one // they caught, up to full when the habit says complex; it never drags a clear task // down to quiet (学行为习惯,不误打断). func (d *Decider) mergeL4(s State, c float64, t string, l4s State, l4c float64, l4t string, l4ok bool) (State, float64, string) { if !l4ok || (l4s != StateAwake && l4s != StateFull) { return s, c, t } if s == StateQuiet || s == "" { // 保守红线:静息→唤醒是"举手示意",即便习惯标记复杂也不直接跳到全能。 return StateAwake, l4c, l4t } st := s if l4s == StateFull { st = StateFull } return st, boostConf(c, l4c), pickTool(t, l4t) } // awakenVerdict maps an intent-record task kind back to a state + lit tool. func awakenVerdict(kind string) (State, string) { switch kind { case taskKindComplex: return StateFull, "" case taskKindCode: return StateAwake, "file" case taskKindQuery: return StateAwake, "web-search" case taskKindOps: return StateAwake, "terminal" default: return StateAwake, "" } } // taskKindFor maps a (state, tool) to the intent kind recorded for writeback. func taskKindFor(state State, tool string) string { if state == StateFull { return taskKindComplex } switch tool { case "file": return taskKindCode case "web-search", "web-fetch", "http": return taskKindQuery case "terminal": return taskKindOps default: return "" } } // allowedHabitScope reports the scopes L4 may read for user habits. ScopeUser is the // owner's shared habit (primary); ScopePersona / ScopeProject are readable per design // §2.2; an empty scope (recall without scope metadata) is tolerated. Any other scope // is refused, so a stranger / owner-private memory can never reach L4 here (design // §2.2 红线: 学行为习惯, 不读陌生人敏感). func allowedHabitScope(scope string) bool { switch scope { case "", "user", "persona", "project": return true default: return false } } // leansTask reports whether a habit memory is about the user's task-expression // pattern rather than an unrelated preference, so L4 does not wake on a coffee habit. func leansTask(content string) bool { return containsAny(content, []string{ "办事", "任务", "布置", "要办", "让我", "要我", "去做", "来办", "干活", "做工", "习惯用", "习惯说", "让我做", "叫我", // ASCII "task", "asks me to", "want me to", "has me", "habit", }) } // overlapLen returns the length (in runes) of the longest common substring of a and // b — the lexical-overlap measure L4 uses to detect "the user expression this round // echoes a phrase the memory recorded", tolerating paraphrase (搞一下那个部署 vs // 搞一下这个部署 share 搞一下). func overlapLen(a, b string) int { ar, br := []rune(a), []rune(b) if len(ar) == 0 || len(br) == 0 { return 0 } prev := make([]int, len(br)+1) best := 0 for i := 1; i <= len(ar); i++ { cur := make([]int, len(br)+1) for j := 1; j <= len(br); j++ { if ar[i-1] == br[j-1] { cur[j] = prev[j-1] + 1 if cur[j] > best { best = cur[j] } } } prev = cur } return best } // boostConf raises a confirmed task confidence above the stronger of the two sources, // reflecting corroboration (设计: 越用越准), capped at 1. func boostConf(a, b float64) float64 { return clampConf(math.Max(a, b) + 0.05) } // clampConf clamps c to [0,1]. func clampConf(c float64) float64 { if c < 0 { return 0 } if c > 1 { return 1 } return c } // pickTool prefers the learned habit's lit tool when non-empty, else the caller's. func pickTool(t, l4t string) string { if l4t != "" { return l4t } return t } // judgmentFor builds the write-back judgment from the raw intent-layer result. func judgmentFor(in Input, state State, conf float64, tool string) Judgment { j := Judgment{State: state, Confidence: conf} if n := len(in.Messages); n > 0 { j.Text = in.Messages[n-1] } j.IsTask = state == StateAwake || state == StateFull if j.IsTask { j.TaskKind = taskKindFor(state, tool) } return j }